sarj-python-lint 1.36.1__tar.gz → 1.38.0__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.36.1 → sarj_python_lint-1.38.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rule_base.py +1 -0
- sarj_python_lint-1.38.0/src/sarj_python_lint/rules/__init__.py +3 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_registry.py +9 -0
- sarj_python_lint-1.38.0/src/sarj_python_lint/rules/no_async_function_cache.py +179 -0
- sarj_python_lint-1.38.0/src/sarj_python_lint/rules/test_without_explicit_oracle.py +244 -0
- sarj_python_lint-1.36.1/src/sarj_python_lint/rules/__init__.py +0 -3
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/.gitignore +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/LICENSE +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/README.md +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/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.
|
|
3
|
+
Version: 1.38.0
|
|
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/
|
|
@@ -32,6 +32,7 @@ from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import (
|
|
|
32
32
|
NoAnalyticalAggregationInPostgresStore,
|
|
33
33
|
)
|
|
34
34
|
from sarj_python_lint.rules.no_any_mapping_types import NoAnyMappingTypes
|
|
35
|
+
from sarj_python_lint.rules.no_async_function_cache import NoAsyncFunctionCache
|
|
35
36
|
from sarj_python_lint.rules.no_before_validator_input_mutation import NoBeforeValidatorInputMutation
|
|
36
37
|
from sarj_python_lint.rules.no_broad_keyword_capture import NoBroadKeywordCapture
|
|
37
38
|
from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
|
|
@@ -215,10 +216,12 @@ from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossi
|
|
|
215
216
|
from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
|
|
216
217
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
217
218
|
from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
|
|
219
|
+
from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
|
|
218
220
|
|
|
219
221
|
|
|
220
222
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
221
223
|
{
|
|
224
|
+
NoExplicitTestOracle.id: NoExplicitTestOracle,
|
|
222
225
|
NoConflictingPydanticAliasValidation.id: NoConflictingPydanticAliasValidation,
|
|
223
226
|
NoUnknownPydanticValidatorField.id: NoUnknownPydanticValidatorField,
|
|
224
227
|
NoImpossiblePydanticFieldBounds.id: NoImpossiblePydanticFieldBounds,
|
|
@@ -239,6 +242,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
239
242
|
NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
|
|
240
243
|
RequireExplicitPsycopgTransaction.id: RequireExplicitPsycopgTransaction,
|
|
241
244
|
AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
|
|
245
|
+
NoAsyncFunctionCache.id: NoAsyncFunctionCache,
|
|
242
246
|
ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
|
|
243
247
|
ExcessiveCommentary.id: ExcessiveCommentary,
|
|
244
248
|
PreferWalrusAwaitedNoneGuard.id: PreferWalrusAwaitedNoneGuard,
|
|
@@ -367,3 +371,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
367
371
|
RepeatedUnimplementedTestContract.id: RepeatedUnimplementedTestContract,
|
|
368
372
|
}
|
|
369
373
|
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
DEFAULT_REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
377
|
+
{name: rule for name, rule in REGISTRY.items() if rule.enabled_by_default}
|
|
378
|
+
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._ast_index import children
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
24
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_FUNCTOOLS = frozenset({"functools"})
|
|
28
|
+
_COROUTINE_CACHES = frozenset({"cache", "cached_property", "lru_cache"})
|
|
29
|
+
_LRU_CACHE_POSITIONAL_ARGUMENTS = 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@final
|
|
33
|
+
class NoAsyncFunctionCache(Rule):
|
|
34
|
+
id = "no-async-function-cache"
|
|
35
|
+
code = "SARJ507"
|
|
36
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
37
|
+
default_level=Severity.WARNING,
|
|
38
|
+
summary="Standard library cache decorates a coroutine function and retains single-use coroutine objects.",
|
|
39
|
+
rationale=(
|
|
40
|
+
"`functools.cache`, `lru_cache`, and `cached_property` store the coroutine object returned by an async "
|
|
41
|
+
"function rather than its awaited value. A subsequent cache hit can raise `RuntimeError: cannot reuse "
|
|
42
|
+
"already awaited coroutine`."
|
|
43
|
+
),
|
|
44
|
+
remediation="Remove the standard library cache or use an async-aware cache that stores completed results.",
|
|
45
|
+
category=RuleCategory.CORRECTNESS,
|
|
46
|
+
autofix=AutofixPolicy.NONE,
|
|
47
|
+
limitations=(
|
|
48
|
+
"Only a proven functools import used as the innermost decorator on a coroutine function is reported.",
|
|
49
|
+
"Async generators, transformed functions, ambiguous or rebound imports, and generated files are excluded.",
|
|
50
|
+
"Wildcard imports and names rebound by relative imports or imports outside the module body are excluded conservatively.",
|
|
51
|
+
"lru_cache with disabled, dynamic, or unpacked maxsize arguments is excluded; an omitted size or literal positive integer or None is required.",
|
|
52
|
+
"Indirect wrapping calls and runtime monkeypatches are not resolved. Test code is checked because coroutine reuse fails there too.",
|
|
53
|
+
"No automatic rewrite is offered because cache lifetime, cancellation, and concurrency require an explicit design decision.",
|
|
54
|
+
),
|
|
55
|
+
examples=(
|
|
56
|
+
RuleExample(
|
|
57
|
+
example_id="cached-coroutine-function",
|
|
58
|
+
title="A standard cache retains an exhausted coroutine",
|
|
59
|
+
outcome=ExampleOutcome.MATCH,
|
|
60
|
+
files=(
|
|
61
|
+
ExampleFile.python(
|
|
62
|
+
"app/cache.py",
|
|
63
|
+
"from functools import cache\n\n@cache\nasync def load() -> int:\n return 1\n",
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
focus_path=PurePosixPath("app/cache.py"),
|
|
67
|
+
expected_count=1,
|
|
68
|
+
public=True,
|
|
69
|
+
),
|
|
70
|
+
RuleExample(
|
|
71
|
+
example_id="fresh-coroutine-function",
|
|
72
|
+
title="Each call creates a fresh coroutine",
|
|
73
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
74
|
+
files=(ExampleFile.python("app/cache.py", "async def load() -> int:\n return 1\n"),),
|
|
75
|
+
focus_path=PurePosixPath("app/cache.py"),
|
|
76
|
+
expected_count=0,
|
|
77
|
+
public=True,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
description = documentation.summary
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
85
|
+
if "async" not in context.source or context.generated or context.tree is None:
|
|
86
|
+
return []
|
|
87
|
+
shadowed = _nested_import_names(context)
|
|
88
|
+
if "*" in shadowed:
|
|
89
|
+
return []
|
|
90
|
+
diagnostics: list[Diagnostic] = []
|
|
91
|
+
for function in context.nodes(ast.AsyncFunctionDef):
|
|
92
|
+
if not function.decorator_list:
|
|
93
|
+
continue
|
|
94
|
+
decorator = function.decorator_list[-1]
|
|
95
|
+
if (
|
|
96
|
+
not _is_coroutine_cache(decorator, context.imports, shadowed)
|
|
97
|
+
or _has_own_yield(function)
|
|
98
|
+
or is_suppressed(context.source_lines, decorator.lineno, self.code)
|
|
99
|
+
):
|
|
100
|
+
continue
|
|
101
|
+
diagnostics.append(
|
|
102
|
+
Diagnostic(
|
|
103
|
+
path=context.path,
|
|
104
|
+
line=decorator.lineno,
|
|
105
|
+
col=decorator.col_offset + 1,
|
|
106
|
+
code=self.code,
|
|
107
|
+
message=(
|
|
108
|
+
"Standard library cache retains a single-use coroutine; remove it or use an async-aware "
|
|
109
|
+
"cache that stores awaited results."
|
|
110
|
+
),
|
|
111
|
+
severity=Severity.WARNING,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
return diagnostics
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _nested_import_names(context: PythonFileContext) -> frozenset[str]:
|
|
118
|
+
return frozenset(
|
|
119
|
+
alias.asname or alias.name.partition(".")[0]
|
|
120
|
+
for node in context.nodes(ast.Import, ast.ImportFrom)
|
|
121
|
+
for alias in node.names
|
|
122
|
+
if context.parents[node] is not context.tree
|
|
123
|
+
or (isinstance(node, ast.ImportFrom) and node.level > 0)
|
|
124
|
+
or alias.name == "*"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _is_coroutine_cache(decorator: ast.expr, imports: ImportIndex, shadowed: frozenset[str]) -> bool:
|
|
129
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
130
|
+
root = target
|
|
131
|
+
while isinstance(root, ast.Attribute):
|
|
132
|
+
root = root.value
|
|
133
|
+
if isinstance(root, ast.Name) and root.id in shadowed:
|
|
134
|
+
return False
|
|
135
|
+
symbol = imports.resolved_symbol(target, sources=_FUNCTOOLS)
|
|
136
|
+
if symbol not in _COROUTINE_CACHES:
|
|
137
|
+
return False
|
|
138
|
+
if not isinstance(decorator, ast.Call):
|
|
139
|
+
return True
|
|
140
|
+
if symbol != "lru_cache" or any(keyword.arg is None for keyword in decorator.keywords):
|
|
141
|
+
return False
|
|
142
|
+
if len(decorator.args) > _LRU_CACHE_POSITIONAL_ARGUMENTS or any(
|
|
143
|
+
isinstance(argument, ast.Starred) for argument in decorator.args
|
|
144
|
+
):
|
|
145
|
+
return False
|
|
146
|
+
sizes = [keyword.value for keyword in decorator.keywords if keyword.arg == "maxsize"]
|
|
147
|
+
if decorator.args:
|
|
148
|
+
sizes.append(decorator.args[0])
|
|
149
|
+
if not sizes:
|
|
150
|
+
return True
|
|
151
|
+
if len(sizes) != 1:
|
|
152
|
+
return False
|
|
153
|
+
match sizes[0]:
|
|
154
|
+
case ast.Constant(value=None):
|
|
155
|
+
return True
|
|
156
|
+
case ast.Constant(value=int() as size) | ast.UnaryOp(op=ast.UAdd(), operand=ast.Constant(value=int() as size)):
|
|
157
|
+
return size > 0
|
|
158
|
+
case _:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _has_own_yield(function: ast.AsyncFunctionDef) -> bool:
|
|
163
|
+
pending: list[ast.AST] = list(function.body)
|
|
164
|
+
while pending:
|
|
165
|
+
match pending.pop():
|
|
166
|
+
case ast.Yield() | ast.YieldFrom():
|
|
167
|
+
return True
|
|
168
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.Lambda() as definition:
|
|
169
|
+
pending.extend(definition.args.defaults)
|
|
170
|
+
pending.extend(default for default in definition.args.kw_defaults if default is not None)
|
|
171
|
+
if not isinstance(definition, ast.Lambda):
|
|
172
|
+
pending.extend(definition.decorator_list)
|
|
173
|
+
case ast.ClassDef() as definition:
|
|
174
|
+
pending.extend(definition.bases)
|
|
175
|
+
pending.extend(keyword.value for keyword in definition.keywords)
|
|
176
|
+
pending.extend(definition.decorator_list)
|
|
177
|
+
case node:
|
|
178
|
+
pending.extend(children(node))
|
|
179
|
+
return False
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
)
|
|
18
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
19
|
+
from sarj_python_lint.rules._pytest import uses_benchmark_fixture
|
|
20
|
+
from sarj_python_lint.rules._test_assertions import names_verification, reads_as_verification
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterator, Sequence
|
|
25
|
+
|
|
26
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
27
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
type _Function = ast.FunctionDef | ast.AsyncFunctionDef
|
|
31
|
+
_NON_UNIT_MARKS = frozenset({"benchmark", "lto", "lto_judge", "skip", "skipif", "xfail"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@final
|
|
35
|
+
class NoExplicitTestOracle(Rule):
|
|
36
|
+
id = "test-without-explicit-oracle"
|
|
37
|
+
code = "SARJ495"
|
|
38
|
+
enabled_by_default = False
|
|
39
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
40
|
+
default_level=Severity.WARNING,
|
|
41
|
+
summary="A collected test has no visible assertion or exception oracle.",
|
|
42
|
+
rationale=(
|
|
43
|
+
"A test can execute a side effect without observing whether the intended effect occurred. "
|
|
44
|
+
"The absence of an explicit oracle is a review candidate, not proof that a smoke or no-throw contract is weak."
|
|
45
|
+
),
|
|
46
|
+
remediation=(
|
|
47
|
+
"Identify the intended contract. Reuse adequate existing coverage, observe the returned value, state, "
|
|
48
|
+
"or emitted effect, or retain the test when successful execution is itself the contract. Do not add "
|
|
49
|
+
"a tautological assertion or delete a test solely to silence this advisory."
|
|
50
|
+
),
|
|
51
|
+
category=RuleCategory.TESTING,
|
|
52
|
+
autofix=AutofixPolicy.NONE,
|
|
53
|
+
limitations=(
|
|
54
|
+
"This opt-in advisory is excluded from default Python checks; select it explicitly or use code-standards observe.",
|
|
55
|
+
"Only default-collected test_* functions and methods of undecorated or pytest-marked, constructor-free, base-free Test* classes are inspected.",
|
|
56
|
+
"Assertions, explicit raises, verification-named calls, import aliases, and local helpers containing or forwarding an oracle are accepted conservatively.",
|
|
57
|
+
"Unknown decorators, test opt-outs, benchmark/LLM/skipped tests, and callable fixture parameters are left unreported; external helper behavior is not inferred.",
|
|
58
|
+
"An intentional no-throw test may match. Findings describe syntax and require contract review; they never authorize automatic deletion or error-level promotion.",
|
|
59
|
+
),
|
|
60
|
+
examples=(
|
|
61
|
+
RuleExample(
|
|
62
|
+
example_id="unobserved-effect",
|
|
63
|
+
title="Observe whether the requested side effect occurred",
|
|
64
|
+
outcome=ExampleOutcome.MATCH,
|
|
65
|
+
files=(ExampleFile.python("tests/test_delivery.py", "def test_delivery():\n deliver('hello')\n"),),
|
|
66
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
67
|
+
expected_count=1,
|
|
68
|
+
public=True,
|
|
69
|
+
),
|
|
70
|
+
RuleExample(
|
|
71
|
+
example_id="observed-effect",
|
|
72
|
+
title="The assertion distinguishes a missing delivery",
|
|
73
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
74
|
+
files=(
|
|
75
|
+
ExampleFile.python(
|
|
76
|
+
"tests/test_delivery.py",
|
|
77
|
+
"def test_delivery(sender):\n deliver('hello', sender)\n assert sender.messages == ['hello']\n",
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
81
|
+
expected_count=0,
|
|
82
|
+
public=True,
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
description = documentation.summary
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
90
|
+
if not is_test_path(context.path) or context.path.name == "conftest.py" or context.generated:
|
|
91
|
+
return []
|
|
92
|
+
tree = context.tree
|
|
93
|
+
if tree is None or _opts_out(tree.body) or not _scope_marks(tree.body, context.imports):
|
|
94
|
+
return []
|
|
95
|
+
helpers = _oracle_helpers(tree, context.imports)
|
|
96
|
+
return [
|
|
97
|
+
Diagnostic(
|
|
98
|
+
path=context.path,
|
|
99
|
+
line=function.lineno,
|
|
100
|
+
col=function.col_offset + 1,
|
|
101
|
+
code=self.code,
|
|
102
|
+
severity=Severity.WARNING,
|
|
103
|
+
message=(
|
|
104
|
+
f"`{function.name}` has no visible assertion or exception oracle. Review the contract: "
|
|
105
|
+
"observe the outcome, reuse existing coverage, or retain an intentional smoke/no-throw test."
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
for function in _collected_tests(tree, context.imports)
|
|
109
|
+
if not uses_benchmark_fixture(function)
|
|
110
|
+
and not _has_oracle(function, helpers, context.imports)
|
|
111
|
+
and not _calls_fixture(function)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _scope_nodes(function: _Function) -> Iterator[ast.AST]:
|
|
116
|
+
pending: list[ast.AST] = list(reversed(function.body))
|
|
117
|
+
while pending:
|
|
118
|
+
node = pending.pop()
|
|
119
|
+
yield node
|
|
120
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
121
|
+
pending.extend(reversed(list(ast.iter_child_nodes(node))))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _has_oracle(function: _Function, helpers: frozenset[str], imports: ImportIndex) -> bool:
|
|
125
|
+
for node in _scope_nodes(function):
|
|
126
|
+
if isinstance(node, (ast.Assert, ast.Raise)):
|
|
127
|
+
return True
|
|
128
|
+
if isinstance(node, ast.Call):
|
|
129
|
+
if names_verification(node.func, helpers):
|
|
130
|
+
return True
|
|
131
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr in helpers:
|
|
132
|
+
return True
|
|
133
|
+
qualified = imports.resolved_qualified_name(node.func)
|
|
134
|
+
if qualified is not None and reads_as_verification(qualified.rsplit(".", 1)[-1]):
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _oracle_helpers(tree: ast.Module, imports: ImportIndex) -> frozenset[str]:
|
|
140
|
+
functions = [node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
141
|
+
helpers: frozenset[str] = frozenset()
|
|
142
|
+
while True:
|
|
143
|
+
updated = helpers | frozenset(
|
|
144
|
+
function.name for function in functions if _has_oracle(function, helpers, imports)
|
|
145
|
+
)
|
|
146
|
+
if updated == helpers:
|
|
147
|
+
return helpers
|
|
148
|
+
helpers = updated
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _opts_out(statements: Sequence[ast.stmt]) -> bool:
|
|
152
|
+
return any(
|
|
153
|
+
isinstance(target, ast.Name)
|
|
154
|
+
and target.id == "__test__"
|
|
155
|
+
and (not isinstance(value, ast.Constant) or value.value is not True)
|
|
156
|
+
for target, value in _assignments(statements)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _assignments(statements: Sequence[ast.stmt]) -> Iterator[tuple[ast.expr, ast.expr]]:
|
|
161
|
+
for statement in statements:
|
|
162
|
+
if isinstance(statement, ast.Assign):
|
|
163
|
+
for target in statement.targets:
|
|
164
|
+
yield target, statement.value
|
|
165
|
+
elif isinstance(statement, ast.AnnAssign) and statement.value is not None:
|
|
166
|
+
yield statement.target, statement.value
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _scope_marks(statements: Sequence[ast.stmt], imports: ImportIndex) -> bool:
|
|
170
|
+
for target, value in _assignments(statements):
|
|
171
|
+
if isinstance(target, ast.Name) and target.id == "pytestmark":
|
|
172
|
+
marks = value.elts if isinstance(value, (ast.List, ast.Tuple)) else [value]
|
|
173
|
+
if not _safe_marks(marks, imports):
|
|
174
|
+
return False
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _disabled_tests(statements: Sequence[ast.stmt]) -> frozenset[str]:
|
|
179
|
+
return frozenset(
|
|
180
|
+
ast.unparse(target.value)
|
|
181
|
+
for target, value in _assignments(statements)
|
|
182
|
+
if (
|
|
183
|
+
isinstance(target, ast.Attribute)
|
|
184
|
+
and target.attr == "__test__"
|
|
185
|
+
and (not isinstance(value, ast.Constant) or value.value is not True)
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _safe_marks(decorators: Sequence[ast.expr], imports: ImportIndex) -> bool:
|
|
191
|
+
for decorator in decorators:
|
|
192
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
193
|
+
qualified = imports.resolved_qualified_name(target)
|
|
194
|
+
if qualified is None or not qualified.startswith("pytest.mark."):
|
|
195
|
+
return False
|
|
196
|
+
if qualified.rsplit(".", 1)[-1] in _NON_UNIT_MARKS:
|
|
197
|
+
return False
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _collected_tests(tree: ast.Module, imports: ImportIndex) -> Iterator[_Function]:
|
|
202
|
+
disabled = _disabled_tests(tree.body)
|
|
203
|
+
for statement in tree.body:
|
|
204
|
+
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
205
|
+
if _test_function(statement, disabled, imports):
|
|
206
|
+
yield statement
|
|
207
|
+
elif isinstance(statement, ast.ClassDef) and statement.name not in disabled and _test_class(statement, imports):
|
|
208
|
+
yield from _class_tests(statement, disabled, imports)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _class_tests(node: ast.ClassDef, disabled: frozenset[str], imports: ImportIndex) -> Iterator[_Function]:
|
|
212
|
+
class_disabled = _disabled_tests(node.body) | frozenset(
|
|
213
|
+
name.removeprefix(f"{node.name}.") for name in disabled if name.startswith(f"{node.name}.")
|
|
214
|
+
)
|
|
215
|
+
for child in node.body:
|
|
216
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)) and _test_function(
|
|
217
|
+
child, class_disabled, imports
|
|
218
|
+
):
|
|
219
|
+
yield child
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _test_function(node: _Function, disabled: frozenset[str], imports: ImportIndex) -> bool:
|
|
223
|
+
return node.name.startswith("test_") and node.name not in disabled and _safe_marks(node.decorator_list, imports)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _test_class(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
227
|
+
if not node.name.startswith("Test") or node.bases or node.keywords or _opts_out(node.body):
|
|
228
|
+
return False
|
|
229
|
+
if any(
|
|
230
|
+
isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)) and child.name in {"__init__", "__new__"}
|
|
231
|
+
for child in node.body
|
|
232
|
+
):
|
|
233
|
+
return False
|
|
234
|
+
return _safe_marks(node.decorator_list, imports) and _scope_marks(node.body, imports)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _calls_fixture(function: _Function) -> bool:
|
|
238
|
+
parameters = {
|
|
239
|
+
argument.arg for argument in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
|
|
240
|
+
}
|
|
241
|
+
return any(
|
|
242
|
+
isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in parameters
|
|
243
|
+
for node in _scope_nodes(function)
|
|
244
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/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.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_namespace_access.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_static_json_alias.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.36.1 → sarj_python_lint-1.38.0}/src/sarj_python_lint/rules/_test_provenance.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
|