sarj-python-lint 1.12.0__tar.gz → 1.14.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.12.0 → sarj_python_lint-1.14.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.14.0/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +326 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +94 -11
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/.gitignore +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/LICENSE +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/README.md +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/unused_mock_setup.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.14.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/
|
|
@@ -98,6 +98,7 @@ from sarj_python_lint.rules.no_whole_request_response_payload_in_log import (
|
|
|
98
98
|
from sarj_python_lint.rules.opaque_parametrize_case_needs_id import OpaqueParametrizeCaseNeedsId
|
|
99
99
|
from sarj_python_lint.rules.over_mocked_test import OverMockedTest
|
|
100
100
|
from sarj_python_lint.rules.phase_label_comment import TestPhaseLabelComment
|
|
101
|
+
from sarj_python_lint.rules.prefer_autospec_for_callable_mock import PreferAutospecForCallableMock
|
|
101
102
|
from sarj_python_lint.rules.prefer_class_row import PreferClassRow
|
|
102
103
|
from sarj_python_lint.rules.prefer_collection_comprehension import PreferCollectionComprehension
|
|
103
104
|
from sarj_python_lint.rules.prefer_constant_time_secret_compare import (
|
|
@@ -248,6 +249,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
248
249
|
PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
|
|
249
250
|
PreferMonkeypatchForProcessStateInTest.id: PreferMonkeypatchForProcessStateInTest,
|
|
250
251
|
MockWithoutSpec.id: MockWithoutSpec,
|
|
252
|
+
PreferAutospecForCallableMock.id: PreferAutospecForCallableMock,
|
|
251
253
|
OpaqueParametrizeCaseNeedsId.id: OpaqueParametrizeCaseNeedsId,
|
|
252
254
|
PytestFixtureReturnsBareTuple.id: PytestFixtureReturnsBareTuple,
|
|
253
255
|
StoreGetDelegatesToBulkRead.id: StoreGetDelegatesToBulkRead,
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
is_suppressed,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
21
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Iterable, Mapping
|
|
26
|
+
|
|
27
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_MOCK_SOURCES = frozenset({"unittest.mock"})
|
|
31
|
+
_CONSTRUCTORS = frozenset({"Mock", "MagicMock", "AsyncMock"})
|
|
32
|
+
_CONSTRUCTOR_OPTIONS = frozenset({"spec", "spec_set", "return_value", "name", "unsafe", "autospec"})
|
|
33
|
+
_SAFE_ATTRIBUTES = frozenset(
|
|
34
|
+
{
|
|
35
|
+
"assert_called",
|
|
36
|
+
"assert_called_once",
|
|
37
|
+
"assert_called_with",
|
|
38
|
+
"assert_called_once_with",
|
|
39
|
+
"assert_any_call",
|
|
40
|
+
"assert_has_calls",
|
|
41
|
+
"assert_not_called",
|
|
42
|
+
"assert_awaited",
|
|
43
|
+
"assert_awaited_once",
|
|
44
|
+
"assert_awaited_with",
|
|
45
|
+
"assert_awaited_once_with",
|
|
46
|
+
"assert_any_await",
|
|
47
|
+
"assert_has_awaits",
|
|
48
|
+
"assert_not_awaited",
|
|
49
|
+
"called",
|
|
50
|
+
"call_count",
|
|
51
|
+
"call_args",
|
|
52
|
+
"call_args_list",
|
|
53
|
+
"mock_calls",
|
|
54
|
+
"await_count",
|
|
55
|
+
"await_args",
|
|
56
|
+
"await_args_list",
|
|
57
|
+
"return_value",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
_REFLECTION = frozenset({"globals", "locals", "vars", "eval", "exec", "setattr", "delattr"})
|
|
61
|
+
_REFLECTION_TARGETS = frozenset(f"builtins.{name}" for name in _REFLECTION)
|
|
62
|
+
_SCOPES = (
|
|
63
|
+
ast.Module,
|
|
64
|
+
ast.FunctionDef,
|
|
65
|
+
ast.AsyncFunctionDef,
|
|
66
|
+
ast.ClassDef,
|
|
67
|
+
ast.Lambda,
|
|
68
|
+
ast.ListComp,
|
|
69
|
+
ast.SetComp,
|
|
70
|
+
ast.DictComp,
|
|
71
|
+
ast.GeneratorExp,
|
|
72
|
+
)
|
|
73
|
+
_BAD = (
|
|
74
|
+
"from unittest.mock import Mock\n"
|
|
75
|
+
"def send(*, recipient: str) -> None:\n pass\n"
|
|
76
|
+
"def test_send():\n double = Mock(spec=send)\n double(recipient='sample')\n"
|
|
77
|
+
)
|
|
78
|
+
_GOOD = _BAD.replace("Mock", "create_autospec").replace("spec=send", "send, spec_set=True")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@final
|
|
82
|
+
class PreferAutospecForCallableMock(Rule):
|
|
83
|
+
id = "prefer-autospec-for-callable-mock"
|
|
84
|
+
code = "SARJ474"
|
|
85
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
86
|
+
default_level=Severity.WARNING,
|
|
87
|
+
summary="Prefer autospec for specced function mocks called with explicit keywords.",
|
|
88
|
+
rationale=(
|
|
89
|
+
"Mock, MagicMock and AsyncMock with spec or spec_set allow incompatible arguments during invocation. "
|
|
90
|
+
"Tests that assert only results or call counts can therefore accept a misspelled keyword or stale call shape."
|
|
91
|
+
),
|
|
92
|
+
remediation=(
|
|
93
|
+
"Use create_autospec(function, spec_set=True), preserving configured results. "
|
|
94
|
+
"An async function produces an awaitable mock whose signature is checked when awaited. "
|
|
95
|
+
"Retain a reasoned local exception for a deliberately permissive double."
|
|
96
|
+
),
|
|
97
|
+
category=RuleCategory.TESTING,
|
|
98
|
+
autofix=AutofixPolicy.NONE,
|
|
99
|
+
limitations=(
|
|
100
|
+
"Only authored test files, proven unittest.mock constructors, earlier undecorated non-generic module-local function specs without **kwargs, and unconditional simple mock assignments are analyzed.",
|
|
101
|
+
"Requires a stable binding and at least one direct call with explicit keywords in the same scope. Escapes, aliases, rebinding, dynamic argument unpacking, wildcard imports and reflective namespace mutation are excluded.",
|
|
102
|
+
"Imported functions, class or descriptor specs, wrapping, side effects and uncertain configuration are excluded. AsyncMock requires an async function spec so autospec preserves awaiting. Import and function shadow checks, including exception/pattern captures and importer-root walrus/global writes, are conservative across the file. Proven builtins reflection aliases are excluded.",
|
|
103
|
+
"Exact call-shape assertions such as assert_called_once_with can already validate a spec's signature; this advisory warning asks for validation during invocation. Autospec does not enforce argument value types.",
|
|
104
|
+
"Passing autospec=True to a Mock constructor merely configures an attribute; it does not activate autospeccing. Use create_autospec instead.",
|
|
105
|
+
"The initial bounded private-platform survey found zero direct matches within this scope; this is a preventive contract warning, not evidence of a current platform incident. No autofix or blocking promotion is proposed.",
|
|
106
|
+
),
|
|
107
|
+
examples=(
|
|
108
|
+
RuleExample(
|
|
109
|
+
example_id="specced-function-direct-call",
|
|
110
|
+
title="A function spec leaves invocation permissive",
|
|
111
|
+
outcome=ExampleOutcome.MATCH,
|
|
112
|
+
files=(ExampleFile.python("tests/test_delivery.py", _BAD),),
|
|
113
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
114
|
+
expected_count=1,
|
|
115
|
+
public=True,
|
|
116
|
+
),
|
|
117
|
+
RuleExample(
|
|
118
|
+
example_id="autospecced-function-direct-call",
|
|
119
|
+
title="Autospec checks the function call shape",
|
|
120
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
121
|
+
files=(ExampleFile.python("tests/test_delivery.py", _GOOD),),
|
|
122
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
123
|
+
expected_count=0,
|
|
124
|
+
public=True,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
description = documentation.summary
|
|
129
|
+
|
|
130
|
+
@override
|
|
131
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
132
|
+
if not is_test_path(context.path) or context.generated or "Mock" not in context.source:
|
|
133
|
+
return []
|
|
134
|
+
tree = context.tree
|
|
135
|
+
if tree is None or _ambiguous_namespace(context):
|
|
136
|
+
return []
|
|
137
|
+
specs = _stable_specs(tree)
|
|
138
|
+
if not specs:
|
|
139
|
+
return []
|
|
140
|
+
findings: list[Diagnostic] = []
|
|
141
|
+
scopes = [tree, *context.nodes(ast.FunctionDef, ast.AsyncFunctionDef)]
|
|
142
|
+
for scope in scopes:
|
|
143
|
+
scope_nodes = tuple(walk(scope))
|
|
144
|
+
bindings = _binding_counts(scope_nodes)
|
|
145
|
+
for statement in scope.body:
|
|
146
|
+
candidate = _mock_binding(statement, specs, context)
|
|
147
|
+
if candidate is None:
|
|
148
|
+
continue
|
|
149
|
+
name, call = candidate
|
|
150
|
+
if bindings[name] != 1 or not _closed_fixed_calls(name, call, scope, scope_nodes, context.parents):
|
|
151
|
+
continue
|
|
152
|
+
if is_suppressed(context.source_lines, call.lineno, self.code):
|
|
153
|
+
continue
|
|
154
|
+
findings.append(
|
|
155
|
+
Diagnostic(
|
|
156
|
+
path=context.path,
|
|
157
|
+
line=call.lineno,
|
|
158
|
+
col=call.col_offset + 1,
|
|
159
|
+
code=self.code,
|
|
160
|
+
message="A function spec does not check arguments during invocation; use create_autospec(function, spec_set=True) for this callable mock.",
|
|
161
|
+
severity=Severity.WARNING,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return findings
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _stable_specs(tree: ast.Module) -> dict[str, ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
168
|
+
bindings = _binding_counts(walk(tree))
|
|
169
|
+
return {
|
|
170
|
+
node.name: node
|
|
171
|
+
for node in tree.body
|
|
172
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
173
|
+
and not node.decorator_list
|
|
174
|
+
and not node.type_params
|
|
175
|
+
and node.args.kwarg is None
|
|
176
|
+
and bindings[node.name] == 1
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _binding_counts(nodes: Iterable[ast.AST]) -> Counter[str]:
|
|
181
|
+
counts: Counter[str] = Counter()
|
|
182
|
+
for node in nodes:
|
|
183
|
+
match node:
|
|
184
|
+
case (
|
|
185
|
+
ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
|
|
186
|
+
| ast.arg(arg=name)
|
|
187
|
+
| ast.FunctionDef(name=name)
|
|
188
|
+
| ast.AsyncFunctionDef(name=name)
|
|
189
|
+
| ast.ClassDef(name=name)
|
|
190
|
+
| ast.ExceptHandler(name=str(name))
|
|
191
|
+
| ast.MatchAs(name=str(name))
|
|
192
|
+
| ast.MatchStar(name=str(name))
|
|
193
|
+
| ast.MatchMapping(rest=str(name))
|
|
194
|
+
):
|
|
195
|
+
counts[name] += 1
|
|
196
|
+
case ast.alias(name=name, asname=alias):
|
|
197
|
+
counts[alias or name.partition(".")[0]] += 1
|
|
198
|
+
case ast.Global(names=names) | ast.Nonlocal(names=names):
|
|
199
|
+
counts.update(names)
|
|
200
|
+
case _:
|
|
201
|
+
pass
|
|
202
|
+
return counts
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _mock_binding(
|
|
206
|
+
statement: ast.stmt, specs: Mapping[str, ast.FunctionDef | ast.AsyncFunctionDef], context: PythonFileContext
|
|
207
|
+
) -> tuple[str, ast.Call] | None:
|
|
208
|
+
match statement:
|
|
209
|
+
case (
|
|
210
|
+
ast.Assign(targets=[ast.Name(id=name)], value=ast.Call() as call)
|
|
211
|
+
| ast.AnnAssign(target=ast.Name(id=name), value=ast.Call() as call)
|
|
212
|
+
):
|
|
213
|
+
pass
|
|
214
|
+
case _:
|
|
215
|
+
return None
|
|
216
|
+
constructor = context.imports.resolved_symbol(call.func, sources=_MOCK_SOURCES)
|
|
217
|
+
if call.args or constructor not in _CONSTRUCTORS:
|
|
218
|
+
return None
|
|
219
|
+
if any(keyword.arg not in _CONSTRUCTOR_OPTIONS for keyword in call.keywords):
|
|
220
|
+
return None
|
|
221
|
+
contracts = [keyword.value for keyword in call.keywords if keyword.arg in {"spec", "spec_set"}]
|
|
222
|
+
if len(contracts) != 1 or not isinstance(contracts[0], ast.Name) or contracts[0].id not in specs:
|
|
223
|
+
return None
|
|
224
|
+
spec = specs[contracts[0].id]
|
|
225
|
+
if spec.lineno >= call.lineno or (constructor == "AsyncMock" and not isinstance(spec, ast.AsyncFunctionDef)):
|
|
226
|
+
return None
|
|
227
|
+
return name, call
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _closed_fixed_calls(
|
|
231
|
+
name: str,
|
|
232
|
+
constructor: ast.Call,
|
|
233
|
+
scope: ast.Module | ast.FunctionDef | ast.AsyncFunctionDef,
|
|
234
|
+
nodes: tuple[ast.AST, ...],
|
|
235
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
236
|
+
) -> bool:
|
|
237
|
+
called = False
|
|
238
|
+
for node in nodes:
|
|
239
|
+
if not isinstance(node, ast.Name) or node.id != name or not isinstance(node.ctx, ast.Load):
|
|
240
|
+
continue
|
|
241
|
+
if node.lineno <= constructor.lineno or _scope_of(node, parents) is not scope:
|
|
242
|
+
return False
|
|
243
|
+
parent = parents[node]
|
|
244
|
+
if isinstance(parent, ast.Call) and parent.func is node:
|
|
245
|
+
if not _fixed_arguments(parent):
|
|
246
|
+
return False
|
|
247
|
+
called = called or bool(parent.keywords)
|
|
248
|
+
elif _safe_mock_attribute(parent):
|
|
249
|
+
continue
|
|
250
|
+
else:
|
|
251
|
+
return False
|
|
252
|
+
return called
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _fixed_arguments(call: ast.Call) -> bool:
|
|
256
|
+
return not any(isinstance(argument, ast.Starred) for argument in call.args) and all(
|
|
257
|
+
keyword.arg is not None for keyword in call.keywords
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _safe_mock_attribute(node: ast.AST) -> bool:
|
|
262
|
+
if not isinstance(node, ast.Attribute) or node.attr not in _SAFE_ATTRIBUTES:
|
|
263
|
+
return False
|
|
264
|
+
return isinstance(node.ctx, ast.Load) or (node.attr == "return_value" and isinstance(node.ctx, ast.Store))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _scope_of(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> ast.AST:
|
|
268
|
+
while node in parents:
|
|
269
|
+
node = parents[node]
|
|
270
|
+
if isinstance(node, _SCOPES):
|
|
271
|
+
return node
|
|
272
|
+
return node
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _ambiguous_namespace(context: PythonFileContext) -> bool:
|
|
276
|
+
if _ambiguous_constructor_bindings(context):
|
|
277
|
+
return True
|
|
278
|
+
for node in context.nodes(ast.ImportFrom, ast.Call, ast.Attribute):
|
|
279
|
+
if isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names):
|
|
280
|
+
return True
|
|
281
|
+
if isinstance(node, ast.Call):
|
|
282
|
+
if isinstance(node.func, ast.Name) and node.func.id in _REFLECTION:
|
|
283
|
+
return True
|
|
284
|
+
if context.imports.resolved_qualified_name(node.func) in _REFLECTION_TARGETS:
|
|
285
|
+
return True
|
|
286
|
+
if (
|
|
287
|
+
isinstance(node, ast.Attribute)
|
|
288
|
+
and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
289
|
+
and context.imports.resolved_symbol(node, sources=_MOCK_SOURCES) in _CONSTRUCTORS
|
|
290
|
+
):
|
|
291
|
+
return True
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _ambiguous_constructor_bindings(context: PythonFileContext) -> bool:
|
|
296
|
+
constructor_roots = _constructor_roots(context)
|
|
297
|
+
for node in context.nodes(
|
|
298
|
+
ast.ExceptHandler, ast.MatchAs, ast.MatchStar, ast.MatchMapping, ast.NamedExpr, ast.Global
|
|
299
|
+
):
|
|
300
|
+
match node:
|
|
301
|
+
case (
|
|
302
|
+
ast.ExceptHandler(name=name)
|
|
303
|
+
| ast.MatchAs(name=name)
|
|
304
|
+
| ast.MatchStar(name=name)
|
|
305
|
+
| ast.MatchMapping(rest=name)
|
|
306
|
+
| ast.NamedExpr(target=ast.Name(id=name))
|
|
307
|
+
):
|
|
308
|
+
if name in constructor_roots:
|
|
309
|
+
return True
|
|
310
|
+
case ast.Global(names=names):
|
|
311
|
+
if constructor_roots.intersection(names):
|
|
312
|
+
return True
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _constructor_roots(context: PythonFileContext) -> set[str]:
|
|
317
|
+
constructor_roots: set[str] = set()
|
|
318
|
+
for call in context.nodes(ast.Call):
|
|
319
|
+
if context.imports.resolved_symbol(call.func, sources=_MOCK_SOURCES) not in _CONSTRUCTORS:
|
|
320
|
+
continue
|
|
321
|
+
root = call.func
|
|
322
|
+
while isinstance(root, ast.Attribute):
|
|
323
|
+
root = root.value
|
|
324
|
+
if isinstance(root, ast.Name):
|
|
325
|
+
constructor_roots.add(root.id)
|
|
326
|
+
return constructor_roots
|
|
@@ -38,14 +38,15 @@ class UnusedTestFactoryOption(Rule):
|
|
|
38
38
|
code = "SARJ443"
|
|
39
39
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
40
40
|
default_level=Severity.WARNING,
|
|
41
|
-
summary="A private test factory exposes
|
|
41
|
+
summary="A private test factory exposes an option that visible callers never vary.",
|
|
42
42
|
rationale="Unused customization obscures the values that actually distinguish test scenarios.",
|
|
43
43
|
remediation="Keep the value in the factory's construction instead of exposing an unused option; retain it if external callers need it.",
|
|
44
44
|
category=RuleCategory.TESTING,
|
|
45
45
|
autofix=AutofixPolicy.NONE,
|
|
46
46
|
limitations=(
|
|
47
47
|
"Only module-level private _make_ and _build_ helpers with straight-line local assignments ending in a construction return are considered; control flow and standalone side effects are excluded.",
|
|
48
|
-
"
|
|
48
|
+
"Literal defaults and stable earlier module-local function defaults are considered. Callable options are reported only when every direct caller omits them; explicit literal options require at least two calls with the same effective value.",
|
|
49
|
+
"Decorators, rebinding, shadowing, callable escapes, reflection, argument unpacking, and ambiguous provider mutation or patch targets exclude the affected helper or callable option.",
|
|
49
50
|
"Cross-module callers cannot be proven absent. Shared helpers require an exact suppression; this advisory never autofixes signatures or changes default evaluation timing.",
|
|
50
51
|
),
|
|
51
52
|
examples=(
|
|
@@ -77,6 +78,36 @@ class UnusedTestFactoryOption(Rule):
|
|
|
77
78
|
expected_count=0,
|
|
78
79
|
public=True,
|
|
79
80
|
),
|
|
81
|
+
RuleExample(
|
|
82
|
+
example_id="unused-callable-option",
|
|
83
|
+
scenario="callable-default",
|
|
84
|
+
title="Use an unexercised local dependency directly",
|
|
85
|
+
outcome=ExampleOutcome.MATCH,
|
|
86
|
+
files=(
|
|
87
|
+
ExampleFile.python(
|
|
88
|
+
"tests/test_widget.py",
|
|
89
|
+
"def _read():\n return 'value'\n\ndef _make_widget(*, read=_read):\n return Widget(read=read)\n\n_make_widget()\n",
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
focus_path=PurePosixPath("tests/test_widget.py"),
|
|
93
|
+
expected_count=1,
|
|
94
|
+
public=True,
|
|
95
|
+
),
|
|
96
|
+
RuleExample(
|
|
97
|
+
example_id="exercised-callable-option",
|
|
98
|
+
scenario="callable-default",
|
|
99
|
+
title="Keep a dependency that a caller replaces",
|
|
100
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
101
|
+
files=(
|
|
102
|
+
ExampleFile.python(
|
|
103
|
+
"tests/test_widget.py",
|
|
104
|
+
"def _read():\n return 'value'\n\ndef _make_widget(*, read=_read):\n return Widget(read=read)\n\n_make_widget(read=other)\n",
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
focus_path=PurePosixPath("tests/test_widget.py"),
|
|
108
|
+
expected_count=0,
|
|
109
|
+
public=True,
|
|
110
|
+
),
|
|
80
111
|
),
|
|
81
112
|
)
|
|
82
113
|
description = documentation.summary
|
|
@@ -173,6 +204,27 @@ def _factory_findings(
|
|
|
173
204
|
),
|
|
174
205
|
)
|
|
175
206
|
)
|
|
207
|
+
for argument, default in _factory_options(function):
|
|
208
|
+
if (
|
|
209
|
+
argument.arg in supplied
|
|
210
|
+
or not isinstance(default, ast.Name)
|
|
211
|
+
or not _is_stable_local_callable(tree, function, default.id, node_index=node_index)
|
|
212
|
+
or is_suppressed(lines, argument.lineno, code)
|
|
213
|
+
):
|
|
214
|
+
continue
|
|
215
|
+
findings.append(
|
|
216
|
+
Diagnostic(
|
|
217
|
+
path=path,
|
|
218
|
+
line=argument.lineno,
|
|
219
|
+
col=argument.col_offset + 1,
|
|
220
|
+
code=code,
|
|
221
|
+
severity=Severity.WARNING,
|
|
222
|
+
message=(
|
|
223
|
+
f"No direct caller in this file supplies `{function.name}.{argument.arg}`; use the stable local "
|
|
224
|
+
f"callable `{default.id}` in the factory instead of exposing an unused option. Retain it if external callers need it."
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
)
|
|
176
228
|
return findings
|
|
177
229
|
|
|
178
230
|
|
|
@@ -258,18 +310,49 @@ def _factory_argument_is_invariant(argument: ast.arg, default: ast.Constant, bou
|
|
|
258
310
|
)
|
|
259
311
|
|
|
260
312
|
|
|
261
|
-
def
|
|
262
|
-
options: list[tuple[ast.arg, ast.Constant]] = []
|
|
313
|
+
def _factory_options(function: ast.FunctionDef) -> list[tuple[ast.arg, ast.expr]]:
|
|
263
314
|
positional = [*function.args.posonlyargs, *function.args.args]
|
|
264
315
|
defaults = [
|
|
265
316
|
*zip(positional[len(positional) - len(function.args.defaults) :], function.args.defaults, strict=True),
|
|
266
317
|
*zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True),
|
|
267
318
|
]
|
|
268
319
|
used = {node.id for node in nodes(function, ast.Name) if isinstance(node.ctx, ast.Load)}
|
|
269
|
-
for argument, default in defaults
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
320
|
+
return [(argument, default) for argument, default in defaults if argument.arg in used and default is not None]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _literal_factory_options(function: ast.FunctionDef) -> list[tuple[ast.arg, ast.Constant]]:
|
|
324
|
+
return [
|
|
325
|
+
(argument, default)
|
|
326
|
+
for argument, default in _factory_options(function)
|
|
327
|
+
if isinstance(default, ast.Constant)
|
|
328
|
+
and (default.value is None or isinstance(default.value, (str, bytes, int, float)))
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _is_stable_local_callable(
|
|
333
|
+
tree: ast.Module,
|
|
334
|
+
factory: ast.FunctionDef,
|
|
335
|
+
name: str,
|
|
336
|
+
*,
|
|
337
|
+
node_index: NodeIndex | None = None,
|
|
338
|
+
) -> bool:
|
|
339
|
+
if not any(
|
|
340
|
+
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
341
|
+
and node.name == name
|
|
342
|
+
and node.lineno < factory.lineno
|
|
343
|
+
and not node.decorator_list
|
|
344
|
+
for node in tree.body
|
|
345
|
+
) or not _has_unambiguous_factory_name(tree, name, node_index=node_index):
|
|
346
|
+
return False
|
|
347
|
+
return not (
|
|
348
|
+
any(node.id == name and not isinstance(node.ctx, ast.Load) for node in nodes(tree, ast.Name, index=node_index))
|
|
349
|
+
or any(
|
|
350
|
+
not isinstance(node.ctx, ast.Load) and any(child.id == name for child in nodes(node.value, ast.Name))
|
|
351
|
+
for node in nodes(tree, ast.Attribute, index=node_index)
|
|
352
|
+
)
|
|
353
|
+
or any(
|
|
354
|
+
isinstance(node.value, str)
|
|
355
|
+
and (node.value.rsplit(".", 1)[-1] == name or factory.name in node.value.split("."))
|
|
356
|
+
for node in nodes(tree, ast.Constant, index=node_index)
|
|
357
|
+
)
|
|
358
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/_test_composition.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_dunder_all.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
|
|
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
|
|
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.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/no_select_star.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_class_row.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
|
|
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.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/prefer_str_enum.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
|
|
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
|
|
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
|
|
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.12.0 → sarj_python_lint-1.14.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|