sarj-python-lint 1.43.0__tar.gz → 1.43.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_resource_provenance.py +10 -6
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +100 -24
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +157 -23
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/.gitignore +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/LICENSE +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/README.md +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/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.43.
|
|
3
|
+
Version: 1.43.1
|
|
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/
|
|
@@ -56,6 +56,15 @@ _RESOURCE_OWNERS = MappingProxyType(
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def builtin_namespace_untouched(context: PythonFileContext) -> bool:
|
|
60
|
+
return not any(
|
|
61
|
+
(isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
|
|
62
|
+
or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
|
|
63
|
+
or (isinstance(node, ast.Name) and node.id == "__builtins__")
|
|
64
|
+
for node in context.nodes(ast.Import, ast.ImportFrom, ast.Name)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
59
68
|
@final
|
|
60
69
|
class ResourceProvenance:
|
|
61
70
|
def __init__(self, context: PythonFileContext, *, require_static_namespace: bool = False) -> None:
|
|
@@ -86,12 +95,7 @@ class ResourceProvenance:
|
|
|
86
95
|
|
|
87
96
|
@cached_property
|
|
88
97
|
def builtin_namespace_untouched(self) -> bool:
|
|
89
|
-
return
|
|
90
|
-
(isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
|
|
91
|
-
or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
|
|
92
|
-
or (isinstance(node, ast.Name) and node.id == "__builtins__")
|
|
93
|
-
for node in self.context.nodes(ast.Import, ast.ImportFrom, ast.Name)
|
|
94
|
-
)
|
|
98
|
+
return builtin_namespace_untouched(self.context)
|
|
95
99
|
|
|
96
100
|
def scope(self, node: ast.AST) -> ast.AST | None:
|
|
97
101
|
parent = self.context.parents.get(node)
|
|
@@ -21,7 +21,7 @@ from sarj_python_lint.rule_base import (
|
|
|
21
21
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
22
22
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
23
23
|
from sarj_python_lint.rules._pytest import collected_tests
|
|
24
|
-
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
24
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance, builtin_namespace_untouched
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
@@ -97,13 +97,44 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
97
97
|
"Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
|
|
98
98
|
"Visible provider replacement, escape or unresolved lexical ownership retains WARNING; only the closed canonical AsyncMock provider proof blocks.",
|
|
99
99
|
"Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
|
|
100
|
-
"Positive call evidence includes direct assertion methods and literal
|
|
101
|
-
"Await evidence excludes provably vacuous checks, including zero or nonnegative
|
|
100
|
+
"Positive call evidence includes direct assertion methods and literal numeric or boolean call-state checks; unresolved call expectations are excluded.",
|
|
101
|
+
"Await evidence excludes provably vacuous checks, including non-None count/list state, zero or nonnegative numeric bounds, and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
|
|
102
|
+
"Length comparisons over await_args_list use count semantics only for an unshadowed builtin len without visible builtin namespace access; custom length functions remain unresolved.",
|
|
102
103
|
"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.",
|
|
103
104
|
"Compound negation and custom expression semantics remain unresolved; no variable values or fixture graphs are inferred.",
|
|
104
105
|
"Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
|
|
105
106
|
),
|
|
106
107
|
examples=(
|
|
108
|
+
RuleExample(
|
|
109
|
+
example_id="vacuous-await-length",
|
|
110
|
+
scenario="await-length-evidence",
|
|
111
|
+
title="An empty await history still has nonnegative length",
|
|
112
|
+
outcome=ExampleOutcome.MATCH,
|
|
113
|
+
files=(
|
|
114
|
+
ExampleFile.python(
|
|
115
|
+
"tests/test_delivery.py",
|
|
116
|
+
"from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n assert len(send.await_args_list) >= 0\n",
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
120
|
+
expected_count=1,
|
|
121
|
+
public=True,
|
|
122
|
+
),
|
|
123
|
+
RuleExample(
|
|
124
|
+
example_id="positive-await-length",
|
|
125
|
+
scenario="await-length-evidence",
|
|
126
|
+
title="A positive await-history length distinguishes a dropped coroutine",
|
|
127
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
128
|
+
files=(
|
|
129
|
+
ExampleFile.python(
|
|
130
|
+
"tests/test_delivery.py",
|
|
131
|
+
"from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n assert len(send.await_args_list) == 1\n",
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
135
|
+
expected_count=0,
|
|
136
|
+
public=True,
|
|
137
|
+
),
|
|
107
138
|
RuleExample(
|
|
108
139
|
example_id="spaced-positive-called-state",
|
|
109
140
|
scenario="called-attribute-spacing",
|
|
@@ -212,6 +243,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
212
243
|
if tree is None:
|
|
213
244
|
return []
|
|
214
245
|
imports = context.module_imports
|
|
246
|
+
builtin_len = builtin_namespace_untouched(context)
|
|
215
247
|
writes = {
|
|
216
248
|
_reference(node)
|
|
217
249
|
for node in context.nodes(ast.AST)
|
|
@@ -221,14 +253,20 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
221
253
|
finding
|
|
222
254
|
for test in collected_tests(tree.body)
|
|
223
255
|
if not _is_pytest_fixture(test, imports)
|
|
224
|
-
for finding in _test_diagnostics(test, context, imports, writes, self.code)
|
|
256
|
+
for finding in _test_diagnostics(test, context, imports, writes, self.code, builtin_len=builtin_len)
|
|
225
257
|
]
|
|
226
258
|
|
|
227
259
|
|
|
228
260
|
def _test_diagnostics(
|
|
229
|
-
test: _TestFunction,
|
|
261
|
+
test: _TestFunction,
|
|
262
|
+
context: PythonFileContext,
|
|
263
|
+
imports: ImportIndex,
|
|
264
|
+
writes: set[_Reference],
|
|
265
|
+
code: str,
|
|
266
|
+
*,
|
|
267
|
+
builtin_len: bool,
|
|
230
268
|
) -> list[Diagnostic]:
|
|
231
|
-
calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes))
|
|
269
|
+
calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes, builtin_len=builtin_len))
|
|
232
270
|
if not calls:
|
|
233
271
|
return []
|
|
234
272
|
severity = Severity.ERROR if _mock_provider_proven(test, context) else Severity.WARNING
|
|
@@ -281,11 +319,11 @@ def _test_imports(module: ImportIndex, test: _TestFunction) -> ImportIndex:
|
|
|
281
319
|
|
|
282
320
|
|
|
283
321
|
def _uncovered_calls(
|
|
284
|
-
imports: ImportIndex, test: _TestFunction, writes: set[_Reference]
|
|
322
|
+
imports: ImportIndex, test: _TestFunction, writes: set[_Reference], *, builtin_len: bool
|
|
285
323
|
) -> Iterator[ast.Call | ast.Attribute]:
|
|
286
324
|
nodes = tuple(walk_ast(test))
|
|
287
325
|
bindings = _mock_bindings(imports, test.body, writes)
|
|
288
|
-
covered = _await_oracles(test.body)
|
|
326
|
+
covered = _await_oracles(test.body, builtin_len=builtin_len and imports.builtin_is_unshadowed("len"))
|
|
289
327
|
reported: set[_Reference] = set()
|
|
290
328
|
for statement in test.body:
|
|
291
329
|
for reference, oracle in _call_oracles(statement):
|
|
@@ -375,7 +413,7 @@ def _is_async_reference(reference: _Reference, constructor: ast.Call) -> bool:
|
|
|
375
413
|
)
|
|
376
414
|
|
|
377
415
|
|
|
378
|
-
def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
|
|
416
|
+
def _await_oracles(statements: list[ast.stmt], *, builtin_len: bool) -> set[_Reference]:
|
|
379
417
|
covered: set[_Reference] = set()
|
|
380
418
|
for statement in statements:
|
|
381
419
|
match statement:
|
|
@@ -385,7 +423,7 @@ def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
|
|
|
385
423
|
if method != "assert_has_awaits" or _calls_evidence(call) is not _Evidence.INSUFFICIENT:
|
|
386
424
|
covered.add(_reference(receiver))
|
|
387
425
|
case ast.Assert(test=condition):
|
|
388
|
-
covered.update(_asserted_states(condition, _AWAIT_STATE))
|
|
426
|
+
covered.update(_asserted_states(condition, _AWAIT_STATE, builtin_len=builtin_len))
|
|
389
427
|
case _:
|
|
390
428
|
pass
|
|
391
429
|
return covered
|
|
@@ -409,17 +447,21 @@ def _sequence_evidence(node: ast.expr) -> _Evidence:
|
|
|
409
447
|
return _Evidence.UNRESOLVED
|
|
410
448
|
|
|
411
449
|
|
|
412
|
-
def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Reference]:
|
|
450
|
+
def _asserted_states(condition: ast.expr, states: frozenset[str], *, builtin_len: bool = False) -> set[_Reference]:
|
|
413
451
|
match condition:
|
|
414
452
|
case ast.UnaryOp(op=ast.Not(), operand=ast.UnaryOp(op=ast.Not(), operand=operand)):
|
|
415
|
-
return _asserted_states(operand, states)
|
|
453
|
+
return _asserted_states(operand, states, builtin_len=builtin_len)
|
|
416
454
|
case ast.UnaryOp(op=ast.Not(), operand=ast.Compare(left=left, ops=[operator], comparators=[right])):
|
|
417
455
|
inverse = _negated_operator(operator)
|
|
418
|
-
return
|
|
456
|
+
return (
|
|
457
|
+
_comparison_states(left, inverse, right, states, builtin_len=builtin_len)
|
|
458
|
+
if inverse is not None
|
|
459
|
+
else set()
|
|
460
|
+
)
|
|
419
461
|
case ast.UnaryOp(op=ast.Not(), operand=ast.Attribute(attr=attribute)) if attribute in states:
|
|
420
462
|
return set()
|
|
421
463
|
case ast.BoolOp(op=operator, values=values):
|
|
422
|
-
branches = [_asserted_states(value, states) for value in values]
|
|
464
|
+
branches = [_asserted_states(value, states, builtin_len=builtin_len) for value in values]
|
|
423
465
|
return (
|
|
424
466
|
branches[0].union(*branches[1:])
|
|
425
467
|
if isinstance(operator, ast.And)
|
|
@@ -430,7 +472,7 @@ def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Refere
|
|
|
430
472
|
case ast.Compare(left=left, ops=operators, comparators=rights):
|
|
431
473
|
result: set[_Reference] = set()
|
|
432
474
|
for first, operator, second in zip((left, *rights[:-1]), operators, rights, strict=True):
|
|
433
|
-
result.update(_comparison_states(first, operator, second, states))
|
|
475
|
+
result.update(_comparison_states(first, operator, second, states, builtin_len=builtin_len))
|
|
434
476
|
return result
|
|
435
477
|
case _:
|
|
436
478
|
# Preserve acceptance of unresolved await-state assertions, such as
|
|
@@ -453,18 +495,37 @@ def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
|
|
|
453
495
|
return factory() if factory is not None else None
|
|
454
496
|
|
|
455
497
|
|
|
456
|
-
def _comparison_states(
|
|
498
|
+
def _comparison_states(
|
|
499
|
+
left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str], *, builtin_len: bool = False
|
|
500
|
+
) -> set[_Reference]:
|
|
457
501
|
result: set[_Reference] = set()
|
|
458
502
|
for node, comparison, expected in ((left, operator, right), (right, _reversed_operator(operator), left)):
|
|
459
|
-
|
|
460
|
-
|
|
503
|
+
state = _state_operand(node, states, builtin_len=builtin_len)
|
|
504
|
+
if state is not None:
|
|
505
|
+
evidence = _state_evidence(
|
|
506
|
+
"await_count" if isinstance(node, ast.Call) else state.attr, comparison, expected
|
|
507
|
+
)
|
|
461
508
|
if evidence is _Evidence.POSITIVE or (states == _AWAIT_STATE and evidence is _Evidence.UNRESOLVED):
|
|
462
|
-
result.add(_reference(
|
|
509
|
+
result.add(_reference(state.value))
|
|
463
510
|
else:
|
|
464
511
|
result.update(_unresolved_await_states(node, states))
|
|
465
512
|
return result
|
|
466
513
|
|
|
467
514
|
|
|
515
|
+
def _state_operand(node: ast.expr, states: frozenset[str], *, builtin_len: bool) -> ast.Attribute | None:
|
|
516
|
+
if isinstance(node, ast.Attribute) and node.attr in states:
|
|
517
|
+
return node
|
|
518
|
+
if builtin_len and states == _AWAIT_STATE:
|
|
519
|
+
match node:
|
|
520
|
+
case ast.Call(
|
|
521
|
+
func=ast.Name(id="len"), args=[ast.Attribute(attr="await_args_list") as argument], keywords=[]
|
|
522
|
+
):
|
|
523
|
+
return argument
|
|
524
|
+
case _:
|
|
525
|
+
pass
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
468
529
|
def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
|
|
469
530
|
match operator:
|
|
470
531
|
case ast.Lt():
|
|
@@ -489,12 +550,14 @@ def _unresolved_await_states(node: ast.expr, states: frozenset[str]) -> set[_Ref
|
|
|
489
550
|
|
|
490
551
|
def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) -> _Evidence:
|
|
491
552
|
positive: bool
|
|
492
|
-
if (
|
|
493
|
-
|
|
553
|
+
if attribute in {"call_count", "await_count"} and (value := _literal_number(expected)) is not None:
|
|
554
|
+
positive = _proves_positive_count(operator, value)
|
|
555
|
+
elif (
|
|
556
|
+
attribute in {"call_count", "await_count", "await_args_list"}
|
|
494
557
|
and isinstance(expected, ast.Constant)
|
|
495
|
-
and
|
|
558
|
+
and expected.value is None
|
|
496
559
|
):
|
|
497
|
-
|
|
560
|
+
return _Evidence.INSUFFICIENT
|
|
498
561
|
elif attribute == "called" and isinstance(expected, ast.Constant) and isinstance(expected.value, bool):
|
|
499
562
|
positive = (isinstance(operator, (ast.Eq, ast.Is)) and expected.value) or (
|
|
500
563
|
isinstance(operator, (ast.NotEq, ast.IsNot)) and not expected.value
|
|
@@ -513,7 +576,20 @@ def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) ->
|
|
|
513
576
|
return _Evidence.POSITIVE if positive else _Evidence.INSUFFICIENT
|
|
514
577
|
|
|
515
578
|
|
|
516
|
-
def
|
|
579
|
+
def _literal_number(node: ast.expr) -> int | float | None:
|
|
580
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
|
581
|
+
return node.value
|
|
582
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant):
|
|
583
|
+
value = node.operand.value
|
|
584
|
+
if isinstance(value, (int, float)):
|
|
585
|
+
if isinstance(node.op, ast.USub):
|
|
586
|
+
return -value
|
|
587
|
+
if isinstance(node.op, ast.UAdd):
|
|
588
|
+
return +value
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _proves_positive_count(operator: ast.cmpop, value: float) -> bool:
|
|
517
593
|
match operator:
|
|
518
594
|
case ast.Eq() | ast.GtE():
|
|
519
595
|
return value > 0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from collections import Counter
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
from typing import TYPE_CHECKING, NamedTuple, final, override
|
|
6
7
|
|
|
@@ -21,7 +22,7 @@ from sarj_python_lint.rules._paths import is_test_path
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
24
|
-
from collections.abc import Mapping
|
|
25
|
+
from collections.abc import Container, Mapping
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
|
|
27
28
|
from sarj_python_lint._file_context import PythonFileContext
|
|
@@ -60,6 +61,11 @@ class _CollectedTest(NamedTuple):
|
|
|
60
61
|
class_bindings: set[str]
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
class _ImportedCollection(NamedTuple):
|
|
65
|
+
module: str
|
|
66
|
+
symbol: str
|
|
67
|
+
|
|
68
|
+
|
|
63
69
|
def _scope_binding_counts(statements: list[ast.stmt]) -> dict[str, int]:
|
|
64
70
|
counts: dict[str, int] = {}
|
|
65
71
|
stack: list[ast.AST] = [*reversed(statements)]
|
|
@@ -115,8 +121,8 @@ def _imported_bindings(
|
|
|
115
121
|
binding_counts: dict[str, int],
|
|
116
122
|
path: Path,
|
|
117
123
|
facts: FirstPartyFacts,
|
|
118
|
-
) -> dict[str,
|
|
119
|
-
bindings: dict[str,
|
|
124
|
+
) -> dict[str, _ImportedCollection]:
|
|
125
|
+
bindings: dict[str, _ImportedCollection] = {}
|
|
120
126
|
for node in tree.body:
|
|
121
127
|
if (
|
|
122
128
|
isinstance(node, ast.ImportFrom)
|
|
@@ -125,7 +131,7 @@ def _imported_bindings(
|
|
|
125
131
|
and _is_first_party_production_module(node.module, path, facts)
|
|
126
132
|
):
|
|
127
133
|
bindings.update(
|
|
128
|
-
(local, node.module)
|
|
134
|
+
(local, _ImportedCollection(node.module, alias.name))
|
|
129
135
|
for alias in node.names
|
|
130
136
|
if alias.name != "*"
|
|
131
137
|
and binding_counts.get(local := alias.asname or alias.name, 0) == 1
|
|
@@ -170,7 +176,7 @@ def _pytest_bindings(tree: ast.Module, binding_counts: dict[str, int]) -> _Pytes
|
|
|
170
176
|
|
|
171
177
|
def _direct_imported_collection(
|
|
172
178
|
node: ast.expr,
|
|
173
|
-
imported: Mapping[str,
|
|
179
|
+
imported: Mapping[str, _ImportedCollection],
|
|
174
180
|
blocked: set[str],
|
|
175
181
|
builtin_wrappers: frozenset[str],
|
|
176
182
|
) -> ast.Name | None:
|
|
@@ -199,7 +205,7 @@ def _is_registry(name: str) -> bool:
|
|
|
199
205
|
|
|
200
206
|
def _is_imported_collection_wrapper(
|
|
201
207
|
node: ast.Call,
|
|
202
|
-
imported: Mapping[str,
|
|
208
|
+
imported: Mapping[str, _ImportedCollection],
|
|
203
209
|
builtin_wrappers: frozenset[str],
|
|
204
210
|
) -> bool:
|
|
205
211
|
return (
|
|
@@ -254,30 +260,134 @@ def _collected_tests(tree: ast.Module) -> list[_CollectedTest]:
|
|
|
254
260
|
|
|
255
261
|
def _independently_asserted_collections(
|
|
256
262
|
tree: ast.Module,
|
|
257
|
-
imported: Mapping[str,
|
|
263
|
+
imported: Mapping[str, _ImportedCollection],
|
|
258
264
|
tests: list[_CollectedTest],
|
|
259
265
|
) -> set[str]:
|
|
260
|
-
expected = _independent_expected_bindings(tree, _scope_binding_counts(tree.body))
|
|
266
|
+
expected = _independent_expected_bindings(tree.body, _scope_binding_counts(tree.body))
|
|
267
|
+
asserted = _asserted_collections(tree.body, imported, expected, blocked=set(), proven_reads={})
|
|
268
|
+
for test in tests:
|
|
269
|
+
if imported.keys() <= asserted:
|
|
270
|
+
break
|
|
271
|
+
function = test.function
|
|
272
|
+
if not any(
|
|
273
|
+
isinstance(statement, ast.Assert)
|
|
274
|
+
and isinstance(statement.test, ast.Compare)
|
|
275
|
+
and any(
|
|
276
|
+
isinstance(operand, ast.Name) and operand.id in imported
|
|
277
|
+
for operand in (statement.test.left, *statement.test.comparators)
|
|
278
|
+
)
|
|
279
|
+
for statement in function.body
|
|
280
|
+
):
|
|
281
|
+
continue
|
|
282
|
+
bindings = _scope_binding_counts(function.body)
|
|
283
|
+
parameters = [*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs]
|
|
284
|
+
if function.args.vararg is not None:
|
|
285
|
+
parameters.append(function.args.vararg)
|
|
286
|
+
if function.args.kwarg is not None:
|
|
287
|
+
parameters.append(function.args.kwarg)
|
|
288
|
+
for parameter in parameters:
|
|
289
|
+
bindings[parameter.arg] = bindings.get(parameter.arg, 0) + 1
|
|
290
|
+
local_expected = _local_expected_bindings(function, bindings)
|
|
291
|
+
proven_reads = _captured_unchanged_defaults(test, imported, bindings)
|
|
292
|
+
proven_reads.update(_direct_production_reimports(function, imported, bindings))
|
|
293
|
+
captured_expected = _captured_unchanged_defaults(test, expected, bindings)
|
|
294
|
+
asserted.update(
|
|
295
|
+
_asserted_collections(
|
|
296
|
+
function.body,
|
|
297
|
+
imported,
|
|
298
|
+
(expected - bindings.keys()) | local_expected | captured_expected.keys(),
|
|
299
|
+
blocked=set(bindings),
|
|
300
|
+
proven_reads=proven_reads,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
return asserted
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _captured_unchanged_defaults(
|
|
307
|
+
test: _CollectedTest, names: Container[str], bindings: dict[str, int]
|
|
308
|
+
) -> dict[str, tuple[int, int]]:
|
|
309
|
+
function = test.function
|
|
310
|
+
defaults = list(zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True))
|
|
311
|
+
if function.args.defaults:
|
|
312
|
+
positional = [*function.args.posonlyargs, *function.args.args]
|
|
313
|
+
defaults.extend(zip(positional[-len(function.args.defaults) :], function.args.defaults, strict=True))
|
|
314
|
+
return {
|
|
315
|
+
argument.arg: (function.lineno, function.col_offset)
|
|
316
|
+
for argument, default in defaults
|
|
317
|
+
if isinstance(default, ast.Name)
|
|
318
|
+
and default.id == argument.arg
|
|
319
|
+
and argument.arg in names
|
|
320
|
+
and argument.arg not in test.class_bindings
|
|
321
|
+
and bindings.get(argument.arg) == 1
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _direct_production_reimports(
|
|
326
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
327
|
+
imported: Mapping[str, _ImportedCollection],
|
|
328
|
+
bindings: dict[str, int],
|
|
329
|
+
) -> dict[str, tuple[int, int]]:
|
|
330
|
+
proven: dict[str, tuple[int, int]] = {}
|
|
331
|
+
for statement in function.body:
|
|
332
|
+
if not isinstance(statement, ast.ImportFrom) or statement.level != 0 or statement.module is None:
|
|
333
|
+
continue
|
|
334
|
+
local_counts = Counter(alias.asname or alias.name for alias in statement.names)
|
|
335
|
+
for alias in statement.names:
|
|
336
|
+
local = alias.asname or alias.name
|
|
337
|
+
if (
|
|
338
|
+
local_counts[local] == 1
|
|
339
|
+
and bindings.get(local) == 1
|
|
340
|
+
and imported.get(local) == _ImportedCollection(statement.module, alias.name)
|
|
341
|
+
):
|
|
342
|
+
proven[local] = (statement.lineno, statement.col_offset)
|
|
343
|
+
return proven
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _local_expected_bindings(function: ast.FunctionDef | ast.AsyncFunctionDef, bindings: dict[str, int]) -> set[str]:
|
|
347
|
+
expected = _independent_expected_bindings(function.body, bindings)
|
|
348
|
+
reads = Counter(
|
|
349
|
+
node.id for node in ast.walk(function) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
|
|
350
|
+
)
|
|
351
|
+
expected.update(
|
|
352
|
+
target.id
|
|
353
|
+
for statement in function.body
|
|
354
|
+
if isinstance(statement, (ast.Assign, ast.AnnAssign)) and _is_literal_table(statement.value)
|
|
355
|
+
for target in _assignment_targets(statement)
|
|
356
|
+
if isinstance(target, ast.Name) and bindings.get(target.id) == 1 and reads[target.id] == 1
|
|
357
|
+
)
|
|
358
|
+
return expected
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _asserted_collections(
|
|
362
|
+
statements: list[ast.stmt],
|
|
363
|
+
imported: Mapping[str, _ImportedCollection],
|
|
364
|
+
expected: set[str],
|
|
365
|
+
*,
|
|
366
|
+
blocked: set[str],
|
|
367
|
+
proven_reads: Mapping[str, tuple[int, int]],
|
|
368
|
+
) -> set[str]:
|
|
261
369
|
asserted: set[str] = set()
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if not isinstance(node.test, ast.Compare):
|
|
370
|
+
for node in statements:
|
|
371
|
+
if not isinstance(node, ast.Assert) or not isinstance(node.test, ast.Compare):
|
|
265
372
|
continue
|
|
266
373
|
compare = node.test
|
|
267
374
|
if len(compare.ops) != 1 or not isinstance(compare.ops[0], ast.Eq) or len(compare.comparators) != 1:
|
|
268
375
|
continue
|
|
376
|
+
visible = (imported.keys() - blocked) | {
|
|
377
|
+
name for name, position in proven_reads.items() if position < (node.lineno, node.col_offset)
|
|
378
|
+
}
|
|
269
379
|
left, right = compare.left, compare.comparators[0]
|
|
270
|
-
if isinstance(left, ast.Name) and left.id in
|
|
380
|
+
if isinstance(left, ast.Name) and left.id in visible and _is_independent_expected(right, expected):
|
|
271
381
|
asserted.add(left.id)
|
|
272
|
-
if isinstance(right, ast.Name) and right.id in
|
|
382
|
+
if isinstance(right, ast.Name) and right.id in visible and _is_independent_expected(left, expected):
|
|
273
383
|
asserted.add(right.id)
|
|
274
384
|
return asserted
|
|
275
385
|
|
|
276
386
|
|
|
277
|
-
def _independent_expected_bindings(
|
|
387
|
+
def _independent_expected_bindings(statements: list[ast.stmt], binding_counts: dict[str, int]) -> set[str]:
|
|
278
388
|
return {
|
|
279
389
|
target.id
|
|
280
|
-
for statement in
|
|
390
|
+
for statement in statements
|
|
281
391
|
if isinstance(statement, (ast.Assign, ast.AnnAssign))
|
|
282
392
|
for target in _assignment_targets(statement)
|
|
283
393
|
if isinstance(target, ast.Name)
|
|
@@ -323,8 +433,40 @@ class ProductionDerivedTestCases(Rule):
|
|
|
323
433
|
"Only proven first-party, non-test imports with eligible, supported, allowed, or required in their names are checked in collected pytest parametrization.",
|
|
324
434
|
"Direct collections, simple unshadowed builtin wrappers, set expressions, and positional or keyword argvalues are recognized.",
|
|
325
435
|
"A direct module-level or collected-test equality against a distinct literal expected table proves an independent oracle; helper-indirected relationships remain unreported.",
|
|
436
|
+
"Collected-test oracles must read the imported production binding without parameter/local shadowing. Expected tuple bindings may be module-level or local and must be assigned once; fresh local literal lists/sets are accepted only when their sole read is the assertion.",
|
|
437
|
+
"Unchanged same-name default captures of production imports or immutable expected tuples, and single direct local reimports of the exact absolute module and symbol, retain provenance. Class-scope default shadowing, later writes, and imports after the assertion do not prove a production read.",
|
|
326
438
|
),
|
|
327
439
|
examples=(
|
|
440
|
+
RuleExample(
|
|
441
|
+
example_id="shadowed-membership-oracle",
|
|
442
|
+
scenario="oracle-lexical-bindings",
|
|
443
|
+
title="A fixture equality does not observe the imported production collection",
|
|
444
|
+
outcome=ExampleOutcome.MATCH,
|
|
445
|
+
files=(
|
|
446
|
+
ExampleFile.python(
|
|
447
|
+
"app/tests/test_models.py",
|
|
448
|
+
"import pytest\nfrom app.models import ELIGIBLE_MODELS\n\ndef test_membership(ELIGIBLE_MODELS):\n assert ELIGIBLE_MODELS == ('a', 'b')\n\n@pytest.mark.parametrize('model', ELIGIBLE_MODELS)\ndef test_model(model):\n assert build(model)\n",
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
focus_path=PurePosixPath("app/tests/test_models.py"),
|
|
452
|
+
expected_count=1,
|
|
453
|
+
public=True,
|
|
454
|
+
),
|
|
455
|
+
RuleExample(
|
|
456
|
+
example_id="local-independent-membership-oracle",
|
|
457
|
+
scenario="oracle-lexical-bindings",
|
|
458
|
+
title="A fresh local expected table observes the actual production collection",
|
|
459
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
460
|
+
files=(
|
|
461
|
+
ExampleFile.python(
|
|
462
|
+
"app/tests/test_models.py",
|
|
463
|
+
"import pytest\nfrom app.models import ELIGIBLE_MODELS\n\ndef test_membership():\n expected = ['a', 'b']\n assert ELIGIBLE_MODELS == expected\n\n@pytest.mark.parametrize('model', ELIGIBLE_MODELS)\ndef test_model(model):\n assert build(model)\n",
|
|
464
|
+
),
|
|
465
|
+
),
|
|
466
|
+
focus_path=PurePosixPath("app/tests/test_models.py"),
|
|
467
|
+
expected_count=0,
|
|
468
|
+
public=True,
|
|
469
|
+
),
|
|
328
470
|
RuleExample(
|
|
329
471
|
example_id="independent-model-cases",
|
|
330
472
|
title="Drive behavior from an independent expected table",
|
|
@@ -424,11 +566,3 @@ def _collect_pytest_modules(node: ast.Import, binding_counts: dict[str, int], mo
|
|
|
424
566
|
for alias in node.names
|
|
425
567
|
if alias.name == "pytest" and binding_counts.get(local := alias.asname or alias.name, 0) == 1
|
|
426
568
|
)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def _direct_assertions(tree: ast.Module, tests: list[_CollectedTest]) -> list[ast.Assert]:
|
|
430
|
-
assertions = [node for node in tree.body if isinstance(node, ast.Assert)]
|
|
431
|
-
assertions.extend(
|
|
432
|
-
statement for test in tests for statement in test.function.body if isinstance(statement, ast.Assert)
|
|
433
|
-
)
|
|
434
|
-
return assertions
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_namespace_access.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|