sarj-python-lint 1.8.0__tar.gz → 1.8.3__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.
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/PKG-INFO +2 -2
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/pyproject.toml +2 -2
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_project_index.py +177 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +124 -74
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +10 -62
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_port_for_service.py +339 -1
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/.gitignore +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/LICENSE +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/README.md +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/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
|
+
Version: 1.8.3
|
|
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/
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Requires-Dist: packaging==26.3
|
|
21
21
|
Requires-Dist: ruff>=0.16.9
|
|
22
|
-
Requires-Dist: sarj-rule-contracts==0.
|
|
22
|
+
Requires-Dist: sarj-rule-contracts==0.2.0
|
|
23
23
|
Requires-Dist: sqlglot==30.19.0
|
|
24
24
|
Requires-Dist: typer==0.27.2
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "1.8.
|
|
3
|
+
version = "1.8.3"
|
|
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,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.14",
|
|
16
16
|
"Topic :: Software Development :: Quality Assurance",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["sarj-rule-contracts==0.
|
|
18
|
+
dependencies = ["sarj-rule-contracts==0.2.0", "packaging==26.3", "ruff>=0.16.9", "sqlglot==30.19.0", "typer==0.27.2"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
{sarj_python_lint-1.8.0 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -4,6 +4,7 @@ import ast
|
|
|
4
4
|
from collections.abc import Mapping
|
|
5
5
|
from contextlib import suppress
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
from functools import cached_property
|
|
7
8
|
import os
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
import re
|
|
@@ -12,6 +13,7 @@ from typing import TYPE_CHECKING, final
|
|
|
12
13
|
|
|
13
14
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
14
15
|
from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
|
|
16
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
if TYPE_CHECKING:
|
|
@@ -179,6 +181,27 @@ class ProjectIndexSet:
|
|
|
179
181
|
consumers.update(_unit_constructor_consumers(candidate, candidate.tree, target))
|
|
180
182
|
return frozenset(consumers)
|
|
181
183
|
|
|
184
|
+
def constructor_operations(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
|
|
185
|
+
if unit.module is None:
|
|
186
|
+
return frozenset()
|
|
187
|
+
return self._constructor_operations.get(SymbolRef(unit.module, name), frozenset())
|
|
188
|
+
|
|
189
|
+
@cached_property
|
|
190
|
+
def _constructor_operations(self) -> Mapping[SymbolRef, frozenset[tuple[Path, str]]]:
|
|
191
|
+
module_counts: dict[str, int] = {}
|
|
192
|
+
for unit in self._units.values():
|
|
193
|
+
if unit.module is not None:
|
|
194
|
+
module_counts[unit.module] = module_counts.get(unit.module, 0) + 1
|
|
195
|
+
owned_modules = frozenset(module for module, count in module_counts.items() if count == 1)
|
|
196
|
+
operations: dict[SymbolRef, set[tuple[Path, str]]] = {}
|
|
197
|
+
for unit in self._units.values():
|
|
198
|
+
if unit.tree is None or unit.module not in owned_modules or is_generated(unit.path, unit.source):
|
|
199
|
+
continue
|
|
200
|
+
for symbol, operation in _unit_constructor_operations(unit, unit.tree, owned_modules):
|
|
201
|
+
if symbol.module in owned_modules and symbol in self._classes:
|
|
202
|
+
operations.setdefault(symbol, set()).add((unit.path, operation))
|
|
203
|
+
return MappingProxyType({symbol: frozenset(calls) for symbol, calls in operations.items()})
|
|
204
|
+
|
|
182
205
|
def direct_subclasses(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
|
|
183
206
|
if unit.module is None:
|
|
184
207
|
return frozenset()
|
|
@@ -554,6 +577,160 @@ def _unit_constructor_consumers(
|
|
|
554
577
|
yield (candidate.path, owner.name)
|
|
555
578
|
|
|
556
579
|
|
|
580
|
+
def _unit_constructor_operations(
|
|
581
|
+
unit: SourceUnit, tree: ast.Module, owned_modules: frozenset[str]
|
|
582
|
+
) -> Iterator[tuple[SymbolRef, str]]:
|
|
583
|
+
if any(
|
|
584
|
+
isinstance(item, ast.ImportFrom) and any(alias.name == "*" for alias in item.names) for item in walk_ast(tree)
|
|
585
|
+
):
|
|
586
|
+
return
|
|
587
|
+
rebound = _module_constructor_rebindings(tree)
|
|
588
|
+
for owner in tree.body:
|
|
589
|
+
self_symbol = SymbolRef(unit.module or "", owner.name) if isinstance(owner, ast.ClassDef) else None
|
|
590
|
+
for method in _direct_functions(owner):
|
|
591
|
+
for symbol, operation in _function_constructor_operations(unit, method, rebound, owned_modules):
|
|
592
|
+
if symbol != self_symbol:
|
|
593
|
+
yield symbol, operation
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _module_constructor_rebindings(tree: ast.Module) -> frozenset[str]:
|
|
597
|
+
names: set[str] = set()
|
|
598
|
+
declarations: dict[str, int] = {}
|
|
599
|
+
for statement in tree.body:
|
|
600
|
+
for name in _module_constructor_declarations(statement):
|
|
601
|
+
declarations[name] = declarations.get(name, 0) + 1
|
|
602
|
+
if isinstance(statement, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
603
|
+
names.add(statement.name)
|
|
604
|
+
elif not isinstance(statement, ast.ClassDef | ast.Import | ast.ImportFrom):
|
|
605
|
+
names.update(_bound_constructor_names(statement))
|
|
606
|
+
names.update(name for name, count in declarations.items() if count > 1)
|
|
607
|
+
return frozenset(names)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _module_constructor_declarations(statement: ast.stmt) -> frozenset[str]:
|
|
611
|
+
if isinstance(statement, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
612
|
+
return frozenset({statement.name})
|
|
613
|
+
return _bound_constructor_names(statement)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _direct_functions(owner: ast.stmt) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ...]:
|
|
617
|
+
if isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
618
|
+
return (owner,)
|
|
619
|
+
if isinstance(owner, ast.ClassDef):
|
|
620
|
+
return tuple(item for item in owner.body if isinstance(item, ast.FunctionDef | ast.AsyncFunctionDef))
|
|
621
|
+
return ()
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _function_constructor_operations(
|
|
625
|
+
unit: SourceUnit,
|
|
626
|
+
method: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
627
|
+
rebound: frozenset[str],
|
|
628
|
+
owned_modules: frozenset[str],
|
|
629
|
+
) -> Iterator[tuple[SymbolRef, str]]:
|
|
630
|
+
shadowed = _bound_constructor_names(method) | rebound
|
|
631
|
+
for statement in method.body:
|
|
632
|
+
if isinstance(statement, ast.Expr | ast.Assign | ast.AnnAssign | ast.Return):
|
|
633
|
+
operation = _direct_constructor_operation(unit, statement.value, shadowed, owned_modules)
|
|
634
|
+
if operation is not None:
|
|
635
|
+
yield operation
|
|
636
|
+
if isinstance(
|
|
637
|
+
statement,
|
|
638
|
+
ast.Return
|
|
639
|
+
| ast.Raise
|
|
640
|
+
| ast.If
|
|
641
|
+
| ast.For
|
|
642
|
+
| ast.AsyncFor
|
|
643
|
+
| ast.While
|
|
644
|
+
| ast.Try
|
|
645
|
+
| ast.TryStar
|
|
646
|
+
| ast.With
|
|
647
|
+
| ast.AsyncWith
|
|
648
|
+
| ast.Match,
|
|
649
|
+
):
|
|
650
|
+
break
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _direct_constructor_operation(
|
|
654
|
+
unit: SourceUnit, value: ast.expr | None, shadowed: frozenset[str], owned_modules: frozenset[str]
|
|
655
|
+
) -> tuple[SymbolRef, str] | None:
|
|
656
|
+
if isinstance(value, ast.Await):
|
|
657
|
+
value = value.value
|
|
658
|
+
if not isinstance(value, ast.Call) or not isinstance(value.func, ast.Attribute):
|
|
659
|
+
return None
|
|
660
|
+
if not isinstance(value.func.value, ast.Call):
|
|
661
|
+
return None
|
|
662
|
+
constructor = value.func.value.func
|
|
663
|
+
root = constructor
|
|
664
|
+
while isinstance(root, ast.Attribute):
|
|
665
|
+
root = root.value
|
|
666
|
+
if not isinstance(root, ast.Name) or root.id in shadowed:
|
|
667
|
+
return None
|
|
668
|
+
symbol = _owned_constructor_symbol(unit, constructor, owned_modules)
|
|
669
|
+
return (symbol, value.func.attr) if symbol is not None else None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _owned_constructor_symbol(
|
|
673
|
+
unit: SourceUnit, expression: ast.expr, owned_modules: frozenset[str]
|
|
674
|
+
) -> SymbolRef | None:
|
|
675
|
+
if isinstance(expression, ast.Name):
|
|
676
|
+
return _resolve(unit, expression)
|
|
677
|
+
parts: list[str] = []
|
|
678
|
+
current = expression
|
|
679
|
+
while isinstance(current, ast.Attribute):
|
|
680
|
+
parts.append(current.attr)
|
|
681
|
+
current = current.value
|
|
682
|
+
if not isinstance(current, ast.Name) or not parts:
|
|
683
|
+
return None
|
|
684
|
+
imported = unit.imports.get(current.id)
|
|
685
|
+
if imported is None or imported.name:
|
|
686
|
+
return None
|
|
687
|
+
ordered = list(reversed(parts))
|
|
688
|
+
module = ".".join((imported.module, *ordered[:-1]))
|
|
689
|
+
if len(ordered) > 1 and not _explicit_constructor_module(unit, module):
|
|
690
|
+
return None
|
|
691
|
+
return SymbolRef(module, ordered[-1]) if module in owned_modules else None
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _explicit_constructor_module(unit: SourceUnit, module: str) -> bool:
|
|
695
|
+
if unit.tree is None:
|
|
696
|
+
return False
|
|
697
|
+
return any(
|
|
698
|
+
alias.name == module and alias.asname is None
|
|
699
|
+
for statement in _module_import_statements(unit.tree)
|
|
700
|
+
if isinstance(statement, ast.Import)
|
|
701
|
+
for alias in statement.names
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _bound_constructor_names(node: ast.AST) -> frozenset[str]:
|
|
706
|
+
names: set[str] = set()
|
|
707
|
+
for item in walk_ast(node):
|
|
708
|
+
match item:
|
|
709
|
+
case (
|
|
710
|
+
ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
|
|
711
|
+
| ast.arg(arg=name)
|
|
712
|
+
| ast.ClassDef(name=name)
|
|
713
|
+
| ast.FunctionDef(name=name)
|
|
714
|
+
| ast.AsyncFunctionDef(name=name)
|
|
715
|
+
| ast.ExceptHandler(name=str(name))
|
|
716
|
+
| ast.MatchAs(name=str(name))
|
|
717
|
+
| ast.MatchStar(name=str(name))
|
|
718
|
+
| ast.MatchMapping(rest=str(name))
|
|
719
|
+
):
|
|
720
|
+
names.add(name)
|
|
721
|
+
case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
|
|
722
|
+
names.update(alias.asname or alias.name.split(".", 1)[0] for alias in aliases)
|
|
723
|
+
case ast.Attribute(ctx=(ast.Store() | ast.Del())):
|
|
724
|
+
root = item.value
|
|
725
|
+
while isinstance(root, ast.Attribute):
|
|
726
|
+
root = root.value
|
|
727
|
+
if isinstance(root, ast.Name):
|
|
728
|
+
names.add(root.id)
|
|
729
|
+
case _:
|
|
730
|
+
pass
|
|
731
|
+
return frozenset(names)
|
|
732
|
+
|
|
733
|
+
|
|
557
734
|
def _class_has_typed_dependency(unit: SourceUnit, owner: ast.ClassDef, target: SymbolRef) -> bool:
|
|
558
735
|
for statement in owner.body:
|
|
559
736
|
if isinstance(statement, ast.AnnAssign) and _annotation_contains_symbol(unit, statement.annotation, target):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from enum import Enum, auto
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
from types import MappingProxyType
|
|
6
7
|
from typing import TYPE_CHECKING, ClassVar, Final, final, override
|
|
@@ -65,12 +66,18 @@ type _TestFunction = ast.FunctionDef | ast.AsyncFunctionDef
|
|
|
65
66
|
type _Reference = tuple[str, ...]
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
class _Evidence(Enum):
|
|
70
|
+
POSITIVE = auto()
|
|
71
|
+
INSUFFICIENT = auto()
|
|
72
|
+
UNRESOLVED = auto()
|
|
73
|
+
|
|
74
|
+
|
|
68
75
|
@final
|
|
69
76
|
class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
70
77
|
id = "async-mock-call-without-await-assertion"
|
|
71
78
|
code = "SARJ456"
|
|
72
79
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
73
|
-
default_level=Severity.
|
|
80
|
+
default_level=Severity.WARNING,
|
|
74
81
|
summary="Pair positive AsyncMock call assertions with evidence that the mock was awaited.",
|
|
75
82
|
rationale=(
|
|
76
83
|
"Calling an AsyncMock records a call before its coroutine is awaited. A positive call assertion can "
|
|
@@ -87,33 +94,50 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
87
94
|
"Only direct statements in collected test functions and methods are inspected; fixtures, helpers, nested blocks, and generated files are excluded.",
|
|
88
95
|
"Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
|
|
89
96
|
"Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
|
|
90
|
-
"Positive call
|
|
91
|
-
"
|
|
97
|
+
"Positive call evidence includes direct assertion methods and literal integer or boolean call-state checks; unresolved call expectations are excluded.",
|
|
98
|
+
"Await evidence excludes provably vacuous checks, including zero or nonnegative counts and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
|
|
99
|
+
"Conjunctions need one accepted await branch; disjunctions need accepted evidence for the same mock in every branch. Argument equality and assertion strength are not compared.",
|
|
100
|
+
"Compound negation and custom expression semantics remain unresolved; no variable values or fixture graphs are inferred.",
|
|
92
101
|
"Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
|
|
93
102
|
),
|
|
94
103
|
examples=(
|
|
95
104
|
RuleExample(
|
|
96
|
-
example_id="
|
|
97
|
-
title="A
|
|
105
|
+
example_id="call-only-async-mock",
|
|
106
|
+
title="A call assertion does not prove awaiting",
|
|
98
107
|
outcome=ExampleOutcome.MATCH,
|
|
99
108
|
files=(
|
|
100
109
|
ExampleFile.python(
|
|
101
110
|
"tests/test_delivery.py",
|
|
102
|
-
"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n
|
|
111
|
+
"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n",
|
|
103
112
|
),
|
|
104
113
|
),
|
|
105
114
|
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
106
115
|
expected_count=1,
|
|
107
|
-
public=
|
|
116
|
+
public=True,
|
|
108
117
|
),
|
|
109
118
|
RuleExample(
|
|
110
|
-
example_id="call-
|
|
111
|
-
title="
|
|
119
|
+
example_id="call-and-await-async-mock",
|
|
120
|
+
title="Call and await assertions protect different contracts",
|
|
121
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
122
|
+
files=(
|
|
123
|
+
ExampleFile.python(
|
|
124
|
+
"tests/test_delivery.py",
|
|
125
|
+
"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n send.assert_awaited_once_with('item')\n",
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
129
|
+
expected_count=0,
|
|
130
|
+
public=True,
|
|
131
|
+
),
|
|
132
|
+
RuleExample(
|
|
133
|
+
example_id="vacuous-await-count",
|
|
134
|
+
scenario="await-expectation-boundaries",
|
|
135
|
+
title="A nonnegative await count does not prove execution",
|
|
112
136
|
outcome=ExampleOutcome.MATCH,
|
|
113
137
|
files=(
|
|
114
138
|
ExampleFile.python(
|
|
115
139
|
"tests/test_delivery.py",
|
|
116
|
-
"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.
|
|
140
|
+
"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n assert send.call_count == 1\n assert send.await_count >= 0\n",
|
|
117
141
|
),
|
|
118
142
|
),
|
|
119
143
|
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
@@ -121,13 +145,14 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
121
145
|
public=True,
|
|
122
146
|
),
|
|
123
147
|
RuleExample(
|
|
124
|
-
example_id="
|
|
125
|
-
|
|
148
|
+
example_id="dynamic-expected-awaits",
|
|
149
|
+
scenario="await-expectation-boundaries",
|
|
150
|
+
title="Unresolved await expectations retain their acceptance",
|
|
126
151
|
outcome=ExampleOutcome.NO_MATCH,
|
|
127
152
|
files=(
|
|
128
153
|
ExampleFile.python(
|
|
129
154
|
"tests/test_delivery.py",
|
|
130
|
-
"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.
|
|
155
|
+
"from unittest.mock import AsyncMock\n\nasync def test_delivery(expected):\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n send.assert_has_awaits(expected)\n",
|
|
131
156
|
),
|
|
132
157
|
),
|
|
133
158
|
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
@@ -166,7 +191,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
166
191
|
line=call.lineno,
|
|
167
192
|
col=call.col_offset + 1,
|
|
168
193
|
code=self.code,
|
|
169
|
-
severity=Severity.
|
|
194
|
+
severity=Severity.WARNING,
|
|
170
195
|
message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
|
|
171
196
|
)
|
|
172
197
|
for test in _collected_tests(tree.body)
|
|
@@ -231,7 +256,7 @@ def _call_oracles(statement: ast.stmt) -> Iterator[tuple[_Reference, ast.Call |
|
|
|
231
256
|
):
|
|
232
257
|
yield _reference(receiver), call
|
|
233
258
|
case ast.Assert(test=condition):
|
|
234
|
-
for reference in sorted(
|
|
259
|
+
for reference in sorted(_asserted_states(condition, _CALL_STATE)):
|
|
235
260
|
oracle = next(
|
|
236
261
|
node
|
|
237
262
|
for node in walk_ast(condition)
|
|
@@ -304,49 +329,60 @@ def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
|
|
|
304
329
|
case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method)) as call) if (
|
|
305
330
|
method in _AWAIT_ASSERTIONS
|
|
306
331
|
):
|
|
307
|
-
if method != "assert_has_awaits" or
|
|
332
|
+
if method != "assert_has_awaits" or _calls_evidence(call) is not _Evidence.INSUFFICIENT:
|
|
308
333
|
covered.add(_reference(receiver))
|
|
309
334
|
case ast.Assert(test=condition):
|
|
310
|
-
covered.update(
|
|
335
|
+
covered.update(_asserted_states(condition, _AWAIT_STATE))
|
|
311
336
|
case _:
|
|
312
337
|
pass
|
|
313
338
|
return covered
|
|
314
339
|
|
|
315
340
|
|
|
316
|
-
def
|
|
341
|
+
def _calls_evidence(call: ast.Call) -> _Evidence:
|
|
317
342
|
if call.args:
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
343
|
+
return _sequence_evidence(call.args[0])
|
|
344
|
+
for keyword in call.keywords:
|
|
345
|
+
if keyword.arg == "calls":
|
|
346
|
+
return _sequence_evidence(keyword.value)
|
|
347
|
+
return _Evidence.UNRESOLVED
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _sequence_evidence(node: ast.expr) -> _Evidence:
|
|
351
|
+
if isinstance(node, (ast.List, ast.Tuple)):
|
|
352
|
+
if not node.elts:
|
|
353
|
+
return _Evidence.INSUFFICIENT
|
|
354
|
+
if any(not isinstance(item, ast.Starred) for item in node.elts):
|
|
355
|
+
return _Evidence.POSITIVE
|
|
356
|
+
return _Evidence.UNRESOLVED
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Reference]:
|
|
360
|
+
match condition:
|
|
361
|
+
case ast.UnaryOp(op=ast.Not(), operand=ast.UnaryOp(op=ast.Not(), operand=operand)):
|
|
362
|
+
return _asserted_states(operand, states)
|
|
363
|
+
case ast.UnaryOp(op=ast.Not(), operand=ast.Compare(left=left, ops=[operator], comparators=[right])):
|
|
364
|
+
inverse = _negated_operator(operator)
|
|
365
|
+
return _comparison_states(left, inverse, right, states) if inverse is not None else set()
|
|
366
|
+
case ast.UnaryOp(op=ast.Not(), operand=ast.Attribute(attr=attribute)) if attribute in states:
|
|
367
|
+
return set()
|
|
368
|
+
case ast.BoolOp(op=operator, values=values):
|
|
369
|
+
branches = [_asserted_states(value, states) for value in values]
|
|
370
|
+
return (
|
|
371
|
+
branches[0].union(*branches[1:])
|
|
372
|
+
if isinstance(operator, ast.And)
|
|
373
|
+
else branches[0].intersection(*branches[1:])
|
|
374
|
+
)
|
|
375
|
+
case ast.Attribute(value=receiver, attr=attribute) if attribute in states:
|
|
376
|
+
return {_reference(receiver)}
|
|
377
|
+
case ast.Compare(left=left, ops=operators, comparators=rights):
|
|
378
|
+
result: set[_Reference] = set()
|
|
379
|
+
for first, operator, second in zip((left, *rights[:-1]), operators, rights, strict=True):
|
|
380
|
+
result.update(_comparison_states(first, operator, second, states))
|
|
381
|
+
return result
|
|
382
|
+
case _:
|
|
383
|
+
# Preserve acceptance of unresolved await-state assertions, such as
|
|
384
|
+
# len(mock.await_args_list) == expected, without value propagation.
|
|
385
|
+
return _unresolved_await_states(condition, states)
|
|
350
386
|
|
|
351
387
|
|
|
352
388
|
def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
|
|
@@ -364,6 +400,18 @@ def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
|
|
|
364
400
|
return factory() if factory is not None else None
|
|
365
401
|
|
|
366
402
|
|
|
403
|
+
def _comparison_states(left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str]) -> set[_Reference]:
|
|
404
|
+
result: set[_Reference] = set()
|
|
405
|
+
for node, comparison, expected in ((left, operator, right), (right, _reversed_operator(operator), left)):
|
|
406
|
+
if isinstance(node, ast.Attribute) and node.attr in states:
|
|
407
|
+
evidence = _state_evidence(node.attr, comparison, expected)
|
|
408
|
+
if evidence is _Evidence.POSITIVE or (states == _AWAIT_STATE and evidence is _Evidence.UNRESOLVED):
|
|
409
|
+
result.add(_reference(node.value))
|
|
410
|
+
else:
|
|
411
|
+
result.update(_unresolved_await_states(node, states))
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
|
|
367
415
|
def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
|
|
368
416
|
match operator:
|
|
369
417
|
case ast.Lt():
|
|
@@ -378,32 +426,38 @@ def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
|
|
|
378
426
|
return operator
|
|
379
427
|
|
|
380
428
|
|
|
381
|
-
def
|
|
382
|
-
if
|
|
429
|
+
def _unresolved_await_states(node: ast.expr, states: frozenset[str]) -> set[_Reference]:
|
|
430
|
+
if states != _AWAIT_STATE:
|
|
383
431
|
return set()
|
|
432
|
+
return {
|
|
433
|
+
_reference(child.value) for child in walk_ast(node) if isinstance(child, ast.Attribute) and child.attr in states
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) -> _Evidence:
|
|
438
|
+
positive: bool
|
|
384
439
|
if (
|
|
385
|
-
|
|
386
|
-
and isinstance(
|
|
387
|
-
and
|
|
388
|
-
and left.attr in {"args", "kwargs"}
|
|
440
|
+
attribute in {"call_count", "await_count"}
|
|
441
|
+
and isinstance(expected, ast.Constant)
|
|
442
|
+
and isinstance(expected.value, int)
|
|
389
443
|
):
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
444
|
+
positive = _proves_positive_count(operator, expected.value)
|
|
445
|
+
elif attribute == "called" and isinstance(expected, ast.Constant) and isinstance(expected.value, bool):
|
|
446
|
+
positive = (isinstance(operator, (ast.Eq, ast.Is)) and expected.value) or (
|
|
447
|
+
isinstance(operator, (ast.NotEq, ast.IsNot)) and not expected.value
|
|
399
448
|
)
|
|
400
|
-
elif
|
|
449
|
+
elif attribute == "await_args" and isinstance(expected, ast.Constant) and expected.value is None:
|
|
401
450
|
positive = isinstance(operator, (ast.NotEq, ast.IsNot))
|
|
402
|
-
elif
|
|
403
|
-
|
|
404
|
-
|
|
451
|
+
elif attribute == "await_args_list" and isinstance(operator, (ast.Eq, ast.NotEq)):
|
|
452
|
+
sequence = _sequence_evidence(expected)
|
|
453
|
+
if sequence is _Evidence.UNRESOLVED:
|
|
454
|
+
return sequence
|
|
455
|
+
positive = (isinstance(operator, ast.Eq) and sequence is _Evidence.POSITIVE) or (
|
|
456
|
+
isinstance(operator, ast.NotEq) and isinstance(expected, ast.List) and sequence is _Evidence.INSUFFICIENT
|
|
405
457
|
)
|
|
406
|
-
|
|
458
|
+
else:
|
|
459
|
+
return _Evidence.UNRESOLVED
|
|
460
|
+
return _Evidence.POSITIVE if positive else _Evidence.INSUFFICIENT
|
|
407
461
|
|
|
408
462
|
|
|
409
463
|
def _proves_positive_count(operator: ast.cmpop, value: int) -> bool:
|
|
@@ -418,10 +472,6 @@ def _proves_positive_count(operator: ast.cmpop, value: int) -> bool:
|
|
|
418
472
|
return False
|
|
419
473
|
|
|
420
474
|
|
|
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
|
-
|
|
425
475
|
def _invalidates_reference(node: ast.AST, reference: _Reference, binding: ast.Name) -> bool:
|
|
426
476
|
match node:
|
|
427
477
|
case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
|