sarj-python-lint 1.28.0__tar.gz → 1.28.2__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.28.0 → sarj_python_lint-1.28.2}/PKG-INFO +1 -1
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/pyproject.toml +1 -1
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/interpreter_argv.py +31 -4
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_resource_provenance.py +10 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +23 -3
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +8 -6
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/unused_mock_setup.py +159 -78
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/.gitignore +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/LICENSE +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/README.md +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/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.28.
|
|
3
|
+
Version: 1.28.2
|
|
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/
|
{sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field, replace
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
5
|
import re
|
|
6
6
|
from types import MappingProxyType
|
|
@@ -22,6 +22,8 @@ class InterpreterInvocation:
|
|
|
22
22
|
kind: ProgramKind
|
|
23
23
|
payload: str | None = None
|
|
24
24
|
forwarded: tuple[str, ...] = ()
|
|
25
|
+
# Runtime options do not change source-payload identity.
|
|
26
|
+
execution_options: frozenset[str] = field(default=frozenset[str](), compare=False)
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
@dataclass(frozen=True, slots=True)
|
|
@@ -374,15 +376,21 @@ def _short_options(
|
|
|
374
376
|
short_seen = False
|
|
375
377
|
bash_mode = shell_name == "bash"
|
|
376
378
|
validated_shell = shell_name in {"bash", "sh", "dash"}
|
|
379
|
+
execution_options: set[str] = set()
|
|
377
380
|
while index < len(arguments):
|
|
378
381
|
boundary = _external_boundary(arguments, index, plus_options=True)
|
|
379
382
|
if boundary is not None and command_mode:
|
|
380
383
|
index += int(arguments[index] in {"-", "--"})
|
|
381
384
|
if index >= len(arguments):
|
|
382
385
|
return InterpreterInvocation("unknown")
|
|
383
|
-
return InterpreterInvocation(
|
|
386
|
+
return InterpreterInvocation(
|
|
387
|
+
"shell", arguments[index], tuple(arguments[index + 2 :]), frozenset(execution_options)
|
|
388
|
+
)
|
|
384
389
|
if boundary is not None:
|
|
385
|
-
return
|
|
390
|
+
return replace(
|
|
391
|
+
InterpreterInvocation("stdin") if stdin_mode else boundary,
|
|
392
|
+
execution_options=frozenset(execution_options),
|
|
393
|
+
)
|
|
386
394
|
argument = arguments[index]
|
|
387
395
|
if argument.startswith("--"):
|
|
388
396
|
parsed = _long_option(arguments, index, shell_mode=shell_mode, bash_mode=bash_mode, short_seen=short_seen)
|
|
@@ -393,10 +401,29 @@ def _short_options(
|
|
|
393
401
|
)
|
|
394
402
|
if parsed.invocation is not None:
|
|
395
403
|
return parsed.invocation
|
|
404
|
+
_record_shell_execution_options(
|
|
405
|
+
execution_options, argument, arguments[index + 1 : index + parsed.consumed], shell_name=shell_name
|
|
406
|
+
)
|
|
396
407
|
stdin_mode = stdin_mode or parsed.stdin_mode
|
|
397
408
|
command_mode = command_mode or parsed.command_mode
|
|
398
409
|
index += parsed.consumed
|
|
399
|
-
return InterpreterInvocation("unknown" if command_mode else "stdin")
|
|
410
|
+
return InterpreterInvocation("unknown" if command_mode else "stdin", execution_options=frozenset(execution_options))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _record_shell_execution_options(
|
|
414
|
+
options: set[str], argument: str, operands: Sequence[str], *, shell_name: str
|
|
415
|
+
) -> None:
|
|
416
|
+
if shell_name not in _SHELLS or argument.startswith("--"):
|
|
417
|
+
return
|
|
418
|
+
values = iter(operands)
|
|
419
|
+
for flag in argument[1:]:
|
|
420
|
+
name = "errexit" if flag == "e" else next(values, None) if flag == "o" else None
|
|
421
|
+
if name is None or name not in {"errexit", "pipefail"}:
|
|
422
|
+
continue
|
|
423
|
+
if argument.startswith("-"):
|
|
424
|
+
options.add(name)
|
|
425
|
+
else:
|
|
426
|
+
options.discard(name)
|
|
400
427
|
|
|
401
428
|
|
|
402
429
|
def _long_option(
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
from collections import defaultdict
|
|
5
|
+
from functools import cached_property
|
|
5
6
|
from typing import TYPE_CHECKING, final
|
|
6
7
|
|
|
7
8
|
|
|
@@ -44,6 +45,15 @@ class ResourceProvenance:
|
|
|
44
45
|
if root is not None:
|
|
45
46
|
self.mutated.add(root)
|
|
46
47
|
|
|
48
|
+
@cached_property
|
|
49
|
+
def builtin_namespace_untouched(self) -> bool:
|
|
50
|
+
return not any(
|
|
51
|
+
(isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
|
|
52
|
+
or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
|
|
53
|
+
or (isinstance(node, ast.Name) and node.id == "__builtins__")
|
|
54
|
+
for node in self.context.nodes(ast.Import, ast.ImportFrom, ast.Name)
|
|
55
|
+
)
|
|
56
|
+
|
|
47
57
|
def scope(self, node: ast.AST) -> ast.AST | None:
|
|
48
58
|
parent = self.context.parents.get(node)
|
|
49
59
|
while parent is not None and not isinstance(parent, _SCOPES):
|
|
@@ -59,8 +59,8 @@ class DuplicateAdjacentExceptionHandler(ProjectRule):
|
|
|
59
59
|
autofix=AutofixPolicy.NONE,
|
|
60
60
|
limitations=(
|
|
61
61
|
"Only ordinary try statements with adjacent handlers, identical exception bindings, executable ASTs and comments are compared; exception groups and bare re-raise/pass handlers are excluded.",
|
|
62
|
-
"Handler types must be simple names or tuples of unshadowed builtin or statically resolved first-party Exception subclasses, with
|
|
63
|
-
"Dynamic namespace access, unknown external exception classes, generated/vendor sources, different comments, and intervening handlers are excluded. No autofix preserves intentional translation seams.",
|
|
62
|
+
"Handler types must be simple names or tuples of unshadowed builtin or statically resolved first-party Exception subclasses, with stable declarations/imports initialized before the enclosing module statement. Declarations after a callable's definition are unresolved and excluded.",
|
|
63
|
+
"Dynamic or explicit builtin namespace access, unknown external exception classes, generated/vendor sources, different comments, and intervening handlers are excluded. Unseen external builtin mutations are not resolved. No autofix preserves intentional translation seams.",
|
|
64
64
|
),
|
|
65
65
|
examples=tuple(
|
|
66
66
|
RuleExample(
|
|
@@ -88,6 +88,8 @@ class DuplicateAdjacentExceptionHandler(ProjectRule):
|
|
|
88
88
|
project = context.session.project or ProjectIndexSet.single(context.path, context.source)
|
|
89
89
|
unit = project.unit_or_source(context.path, context.source, tree)
|
|
90
90
|
provenance = ResourceProvenance(context)
|
|
91
|
+
if not provenance.builtin_namespace_untouched:
|
|
92
|
+
return []
|
|
91
93
|
comments = scan_comments(context.source)
|
|
92
94
|
findings: list[Diagnostic] = []
|
|
93
95
|
for statement in context.nodes(ast.Try):
|
|
@@ -176,7 +178,11 @@ def _first_party_exception(
|
|
|
176
178
|
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer until the rule registry finishes loading.
|
|
177
179
|
|
|
178
180
|
binding = provenance.binding(expression.id, expression)
|
|
179
|
-
if
|
|
181
|
+
if (
|
|
182
|
+
not isinstance(binding, ast.ClassDef | ast.alias)
|
|
183
|
+
or unit is None
|
|
184
|
+
or not _initialized_before(binding, expression, context)
|
|
185
|
+
):
|
|
180
186
|
return False
|
|
181
187
|
if isinstance(binding, ast.alias):
|
|
182
188
|
statement = context.parents.get(binding)
|
|
@@ -200,6 +206,7 @@ def _first_party_exception(
|
|
|
200
206
|
)
|
|
201
207
|
return (
|
|
202
208
|
declaration is not None
|
|
209
|
+
and definition_provenance.builtin_namespace_untouched
|
|
203
210
|
and stable_class(definition_context, declaration, definition_provenance)
|
|
204
211
|
and len(declaration.bases) == 1
|
|
205
212
|
and _safe_types(
|
|
@@ -211,3 +218,16 @@ def _first_party_exception(
|
|
|
211
218
|
seen=seen | {summary.symbol},
|
|
212
219
|
)
|
|
213
220
|
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _initialized_before(binding: ast.ClassDef | ast.alias, read: ast.Name, context: PythonFileContext) -> bool:
|
|
224
|
+
declaration = context.parents.get(binding) if isinstance(binding, ast.alias) else binding
|
|
225
|
+
boundary: ast.AST = read
|
|
226
|
+
while (parent := context.parents.get(boundary)) is not None and parent is not context.tree:
|
|
227
|
+
boundary = parent
|
|
228
|
+
return (
|
|
229
|
+
isinstance(declaration, ast.stmt)
|
|
230
|
+
and context.parents.get(declaration) is context.tree
|
|
231
|
+
and isinstance(boundary, ast.stmt)
|
|
232
|
+
and (declaration.end_lineno or declaration.lineno) < boundary.lineno
|
|
233
|
+
)
|
|
@@ -57,10 +57,10 @@ class OpaqueParametrizeCaseNeedsId(Rule):
|
|
|
57
57
|
code: str = "SARJ042"
|
|
58
58
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
59
59
|
default_level=Severity.WARNING,
|
|
60
|
-
summary="Opaque static pytest parameter cases
|
|
61
|
-
rationale="
|
|
60
|
+
summary="Opaque static pytest parameter cases lack table-local semantic IDs.",
|
|
61
|
+
rationale="Without a semantic naming owner, fallback IDs such as `payload0` are hard to diagnose and silently change when the table is reordered.",
|
|
62
62
|
remediation=(
|
|
63
|
-
"
|
|
63
|
+
"Check the owning pytest_make_parametrize_id hook before adding IDs; reuse an existing semantic naming owner. Otherwise give each opaque row a semantic `pytest.param(..., id=...)`, or use a semantic callable `ids=` when one "
|
|
64
64
|
"mapping applies to the entire table."
|
|
65
65
|
),
|
|
66
66
|
category=RuleCategory.TESTING,
|
|
@@ -129,10 +129,12 @@ class OpaqueParametrizeCaseNeedsId(Rule):
|
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
def _message(count: int) -> str:
|
|
132
|
-
subject = "case
|
|
132
|
+
subject = "case lacks" if count == 1 else "cases lack"
|
|
133
133
|
return (
|
|
134
|
-
f"{count} {subject}
|
|
135
|
-
'
|
|
134
|
+
f"{count} {subject} a table-local semantic ID. Check the owning `pytest_make_parametrize_id` hook before "
|
|
135
|
+
'adding IDs; reuse an existing semantic naming owner, or add semantic `pytest.param(..., id="...")` values '
|
|
136
|
+
"or a semantic callable `ids=`. Without a semantic ID, pytest may use an argument-name-and-index fallback "
|
|
137
|
+
"(for example, `payload0`), which renumbers when rows are reordered."
|
|
136
138
|
)
|
|
137
139
|
|
|
138
140
|
|
{sarj_python_lint-1.28.0 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
import builtins
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
ClassVar,
|
|
9
|
+
NamedTuple,
|
|
10
|
+
cast, # ruff: ignore[banned-api] -- vars exposes Any; the object view permits native exception narrowing.
|
|
11
|
+
override,
|
|
12
|
+
)
|
|
6
13
|
|
|
7
14
|
from sarj_python_lint.rule_base import (
|
|
8
15
|
AutofixPolicy,
|
|
@@ -16,18 +23,23 @@ from sarj_python_lint.rule_base import (
|
|
|
16
23
|
Severity,
|
|
17
24
|
)
|
|
18
25
|
from sarj_python_lint.rules._ast_index import children, walk
|
|
26
|
+
from sarj_python_lint.rules._closed_helpers import dynamic_namespace
|
|
19
27
|
from sarj_python_lint.rules._paths import is_test_path
|
|
28
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
if TYPE_CHECKING:
|
|
23
|
-
from collections.abc import Iterator
|
|
32
|
+
from collections.abc import Iterator, Mapping
|
|
24
33
|
|
|
25
34
|
from sarj_python_lint._file_context import PythonFileContext
|
|
26
|
-
from sarj_python_lint.rules._imports import ImportIndex
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
# The two attributes that *configure* what a mock does when called.
|
|
30
38
|
_CONFIG_ATTRS = frozenset({"return_value", "side_effect"})
|
|
39
|
+
_BUILTIN_NAMESPACE = cast("Mapping[str, object]", vars(builtins))
|
|
40
|
+
_EXCEPTION_TYPES = frozenset(
|
|
41
|
+
name for name, value in _BUILTIN_NAMESPACE.items() if isinstance(value, type) and issubclass(value, BaseException)
|
|
42
|
+
)
|
|
31
43
|
|
|
32
44
|
# Callables whose constructor keywords configure the returned double. Import
|
|
33
45
|
# resolution is required for bare/module-qualified spellings; the pytest-mock
|
|
@@ -45,6 +57,7 @@ _MOCK_CONSTRUCTORS = frozenset(
|
|
|
45
57
|
_MOCKER_CONFIGURATORS = _MOCK_CONSTRUCTORS | {"patch"}
|
|
46
58
|
_UNITTEST_MOCK = frozenset({"unittest.mock"})
|
|
47
59
|
_MOCKER = "mocker"
|
|
60
|
+
_PATCH_OBJECT_POSITIONAL_ARGUMENTS = 2
|
|
48
61
|
|
|
49
62
|
# Assertions that the mock was never reached.
|
|
50
63
|
_NOT_CALLED_ASSERTIONS = frozenset({"assert_not_called", "assert_not_awaited"})
|
|
@@ -89,14 +102,14 @@ class UnusedMockSetup(Rule):
|
|
|
89
102
|
"terminal exact-path assertion proves the configured mock was never called."
|
|
90
103
|
),
|
|
91
104
|
remediation=(
|
|
92
|
-
"Delete the earlier
|
|
105
|
+
"Delete the earlier inert configuration, retaining the mock constructor and binding when removing a keyword, or place the intended action before reconfiguration; "
|
|
93
106
|
"delete behavior configuration contradicted by a terminal never-called assertion."
|
|
94
107
|
),
|
|
95
108
|
category=RuleCategory.TESTING,
|
|
96
109
|
autofix=AutofixPolicy.NONE,
|
|
97
110
|
limitations=(
|
|
98
|
-
"Only non-generated test paths and
|
|
99
|
-
"Only same-block overwrites and exact-path terminal never-called assertions are checked; uncertain evaluation, control flow, reads, and escapes are excluded.",
|
|
111
|
+
"Only non-generated test paths and stable local names unconditionally initialized from unittest.mock or an unshadowed pytest-mock fixture before configuration are analyzed. Dotted stores, auto-children, configured return-value chains, replacement or unpacked options, dynamic namespace access, and direct constructor-root or builtin namespace mutation are excluded. Unseen external mutations are not resolved.",
|
|
112
|
+
"Only same-block overwrites and exact-path terminal never-called assertions are checked; uncertain evaluation, native setter/iteration effects, custom hash/equality, control flow, reads, and terminal receiver escapes are excluded. Configuration values require literal/native primitive provenance.",
|
|
100
113
|
),
|
|
101
114
|
examples=(
|
|
102
115
|
RuleExample(
|
|
@@ -138,14 +151,16 @@ class UnusedMockSetup(Rule):
|
|
|
138
151
|
if not is_test_path(path) or context.generated:
|
|
139
152
|
return []
|
|
140
153
|
tree = context.tree
|
|
141
|
-
if tree is None:
|
|
154
|
+
if tree is None or dynamic_namespace(context):
|
|
142
155
|
return []
|
|
143
156
|
|
|
144
|
-
|
|
157
|
+
provenance = ResourceProvenance(context)
|
|
158
|
+
if not provenance.builtin_namespace_untouched:
|
|
159
|
+
return []
|
|
145
160
|
seen: set[tuple[int, int]] = set()
|
|
146
161
|
diags: list[Diagnostic] = []
|
|
147
162
|
for fn in context.nodes(*_FUNC_NODES):
|
|
148
|
-
for finding in _dead_setups(fn,
|
|
163
|
+
for finding in _dead_setups(fn, provenance):
|
|
149
164
|
position = (finding.node.lineno, finding.node.col_offset + 1)
|
|
150
165
|
if position in seen:
|
|
151
166
|
continue
|
|
@@ -164,20 +179,22 @@ class UnusedMockSetup(Rule):
|
|
|
164
179
|
return diags
|
|
165
180
|
|
|
166
181
|
|
|
167
|
-
def _dead_setups(fn: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
168
|
-
origins = _mock_origins(fn,
|
|
182
|
+
def _dead_setups(fn: ast.FunctionDef | ast.AsyncFunctionDef, provenance: ResourceProvenance) -> Iterator[_Finding]:
|
|
183
|
+
origins = _mock_origins(fn, provenance)
|
|
169
184
|
if not origins:
|
|
170
185
|
return
|
|
171
|
-
yield from _overwritten_before_use(fn,
|
|
172
|
-
yield from _asserted_never_called(fn, origins)
|
|
186
|
+
yield from _overwritten_before_use(fn, origins, provenance)
|
|
187
|
+
yield from _asserted_never_called(fn, origins, provenance)
|
|
173
188
|
|
|
174
189
|
|
|
175
190
|
def _overwritten_before_use(
|
|
176
|
-
fn: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
191
|
+
fn: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
192
|
+
origins: frozenset[str],
|
|
193
|
+
provenance: ResourceProvenance,
|
|
177
194
|
) -> Iterator[_Finding]:
|
|
178
195
|
has_mocker_fixture = _has_unshadowed_mocker_fixture(fn)
|
|
179
196
|
for block in _blocks(fn):
|
|
180
|
-
yield from _overwritten_block_setups(block,
|
|
197
|
+
yield from _overwritten_block_setups(block, origins, provenance, has_mocker_fixture=has_mocker_fixture)
|
|
181
198
|
|
|
182
199
|
|
|
183
200
|
def _has_unshadowed_mocker_fixture(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
@@ -202,39 +219,37 @@ def _owned_nodes(root: ast.AST) -> Iterator[ast.AST]:
|
|
|
202
219
|
stack.extend(reversed(list(ast.iter_child_nodes(node))))
|
|
203
220
|
|
|
204
221
|
|
|
205
|
-
def _mock_origins(fn: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
222
|
+
def _mock_origins(fn: ast.FunctionDef | ast.AsyncFunctionDef, provenance: ResourceProvenance) -> frozenset[str]:
|
|
206
223
|
has_mocker_fixture = _has_unshadowed_mocker_fixture(fn)
|
|
207
|
-
binding_counts: dict[str, int] = {}
|
|
208
|
-
for argument in (*fn.args.posonlyargs, *fn.args.args, *fn.args.kwonlyargs):
|
|
209
|
-
binding_counts[argument.arg] = binding_counts.get(argument.arg, 0) + 1
|
|
210
|
-
for node in _owned_nodes(fn):
|
|
211
|
-
if isinstance(node, (ast.Name, ast.Attribute)) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
212
|
-
path = _dotted(node)
|
|
213
|
-
if path is not None:
|
|
214
|
-
binding_counts[path] = binding_counts.get(path, 0) + 1
|
|
215
224
|
origins: set[str] = set()
|
|
216
|
-
for stmt in
|
|
225
|
+
for stmt in fn.body:
|
|
217
226
|
match stmt:
|
|
218
|
-
case ast.Assign(targets=[target], value=ast.Call() as call):
|
|
227
|
+
case ast.Assign(targets=[ast.Name() as target], value=ast.Call() as call):
|
|
219
228
|
pass
|
|
220
|
-
case ast.AnnAssign(target=target, value=ast.Call() as call):
|
|
229
|
+
case ast.AnnAssign(target=ast.Name() as target, value=ast.Call() as call):
|
|
221
230
|
pass
|
|
222
231
|
case _:
|
|
223
232
|
continue
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
path is not None
|
|
227
|
-
and binding_counts.get(path) == 1
|
|
228
|
-
and _is_mock_configurator(call.func, imports, has_mocker_fixture=has_mocker_fixture)
|
|
233
|
+
if provenance.binding(target.id, call) is target and _is_mock_configurator(
|
|
234
|
+
call, provenance, has_mocker_fixture=has_mocker_fixture
|
|
229
235
|
):
|
|
230
|
-
origins.add(
|
|
236
|
+
origins.add(target.id)
|
|
231
237
|
return frozenset(origins)
|
|
232
238
|
|
|
233
239
|
|
|
234
|
-
def _is_mock_configurator(
|
|
240
|
+
def _is_mock_configurator(call: ast.Call, provenance: ResourceProvenance, *, has_mocker_fixture: bool) -> bool:
|
|
241
|
+
func = call.func
|
|
242
|
+
path = _dotted(func)
|
|
243
|
+
if (
|
|
244
|
+
path is None
|
|
245
|
+
or path.partition(".")[0] in provenance.mutated
|
|
246
|
+
or any(isinstance(argument, ast.Starred) for argument in call.args)
|
|
247
|
+
or any(keyword.arg in {None, "new", "new_callable", "_mock_delegate"} for keyword in call.keywords)
|
|
248
|
+
):
|
|
249
|
+
return False
|
|
235
250
|
if has_mocker_fixture and isinstance(func, ast.Attribute):
|
|
236
251
|
if isinstance(func.value, ast.Name) and func.value.id == _MOCKER:
|
|
237
|
-
return func.attr in _MOCKER_CONFIGURATORS
|
|
252
|
+
return func.attr in _MOCKER_CONFIGURATORS and (func.attr != "patch" or len(call.args) <= 1)
|
|
238
253
|
if (
|
|
239
254
|
func.attr == "object"
|
|
240
255
|
and isinstance(func.value, ast.Attribute)
|
|
@@ -242,11 +257,16 @@ def _is_mock_configurator(func: ast.expr, imports: ImportIndex, *, has_mocker_fi
|
|
|
242
257
|
and isinstance(func.value.value, ast.Name)
|
|
243
258
|
and func.value.value.id == _MOCKER
|
|
244
259
|
):
|
|
245
|
-
return
|
|
246
|
-
return any(
|
|
260
|
+
return len(call.args) <= _PATCH_OBJECT_POSITIONAL_ARGUMENTS
|
|
261
|
+
return any(
|
|
262
|
+
provenance.context.imports.resolves(func, sources=_UNITTEST_MOCK, symbol=symbol)
|
|
263
|
+
for symbol in _MOCK_CONSTRUCTORS
|
|
264
|
+
)
|
|
247
265
|
|
|
248
266
|
|
|
249
|
-
def _asserted_never_called(
|
|
267
|
+
def _asserted_never_called(
|
|
268
|
+
fn: ast.FunctionDef | ast.AsyncFunctionDef, origins: frozenset[str], provenance: ResourceProvenance
|
|
269
|
+
) -> Iterator[_Finding]:
|
|
250
270
|
if not fn.name.startswith("test_") or _is_fixture(fn):
|
|
251
271
|
return
|
|
252
272
|
introspected = _introspected_paths(fn)
|
|
@@ -256,8 +276,14 @@ def _asserted_never_called(fn: ast.FunctionDef | ast.AsyncFunctionDef, origins:
|
|
|
256
276
|
for block in _blocks(fn):
|
|
257
277
|
not_called = _not_called_assertions(block, origins)
|
|
258
278
|
for stmt in block:
|
|
259
|
-
target = _config_target(stmt, origins)
|
|
260
|
-
if
|
|
279
|
+
target = _config_target(stmt, origins, provenance)
|
|
280
|
+
if (
|
|
281
|
+
target is None
|
|
282
|
+
or id(stmt) in loop_statements
|
|
283
|
+
or target in loaded_configs
|
|
284
|
+
or not _inert_configuration(stmt, target, provenance)
|
|
285
|
+
or _receiver_escapes(target.rsplit(".", 1)[0], stmt, provenance)
|
|
286
|
+
):
|
|
261
287
|
continue
|
|
262
288
|
configured = target.rsplit(".", 1)[0]
|
|
263
289
|
if any(_touches(configured, other) for other in introspected):
|
|
@@ -265,6 +291,22 @@ def _asserted_never_called(fn: ast.FunctionDef | ast.AsyncFunctionDef, origins:
|
|
|
265
291
|
yield from _unused_asserted_setup(stmt, target, configured, not_called, last_effect)
|
|
266
292
|
|
|
267
293
|
|
|
294
|
+
def _receiver_escapes(name: str, at: ast.stmt, provenance: ResourceProvenance) -> bool:
|
|
295
|
+
binding = provenance.binding(name, at)
|
|
296
|
+
for read in provenance.reads.get(name, ()):
|
|
297
|
+
if provenance.binding(name, read) is not binding:
|
|
298
|
+
continue
|
|
299
|
+
parent = provenance.context.parents.get(read)
|
|
300
|
+
if not isinstance(parent, ast.Attribute) or parent.value is not read:
|
|
301
|
+
return True
|
|
302
|
+
if isinstance(parent.ctx, ast.Store) and parent.attr in _CONFIG_ATTRS:
|
|
303
|
+
continue
|
|
304
|
+
call = provenance.context.parents.get(parent)
|
|
305
|
+
if not (isinstance(call, ast.Call) and call.func is parent and parent.attr in _NOT_CALLED_ASSERTIONS):
|
|
306
|
+
return True
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
268
310
|
def _is_fixture(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
269
311
|
return any(
|
|
270
312
|
(isinstance(decorator, ast.Name) and decorator.id == "fixture")
|
|
@@ -327,22 +369,19 @@ def _child_blocks(node: ast.AST) -> list[list[ast.stmt]]:
|
|
|
327
369
|
return [block for block in blocks if block]
|
|
328
370
|
|
|
329
371
|
|
|
330
|
-
def
|
|
331
|
-
for block in _blocks(fn):
|
|
332
|
-
yield from block
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def _config_target(stmt: ast.stmt, origins: frozenset[str]) -> str | None:
|
|
372
|
+
def _config_target(stmt: ast.stmt, origins: frozenset[str], provenance: ResourceProvenance) -> str | None:
|
|
336
373
|
if not isinstance(stmt, ast.Assign) or len(stmt.targets) != 1:
|
|
337
374
|
return None
|
|
338
375
|
target = stmt.targets[0]
|
|
339
|
-
if
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
376
|
+
if (
|
|
377
|
+
not isinstance(target, ast.Attribute)
|
|
378
|
+
or target.attr not in _CONFIG_ATTRS
|
|
379
|
+
or not isinstance(target.value, ast.Name)
|
|
380
|
+
or target.value.id not in origins
|
|
381
|
+
or provenance.constructor(target.value, stmt) is None
|
|
382
|
+
):
|
|
343
383
|
return None
|
|
344
|
-
|
|
345
|
-
return path if any(_is_prefix(origin, configured) for origin in origins) else None
|
|
384
|
+
return f"{target.value.id}.{target.attr}"
|
|
346
385
|
|
|
347
386
|
|
|
348
387
|
def _dotted(node: ast.expr) -> str | None:
|
|
@@ -357,20 +396,45 @@ def _dotted(node: ast.expr) -> str | None:
|
|
|
357
396
|
return ".".join(reversed(parts))
|
|
358
397
|
|
|
359
398
|
|
|
360
|
-
def _is_safe_value(node: ast.expr) -> bool:
|
|
399
|
+
def _is_safe_value(node: ast.expr, provenance: ResourceProvenance, *, hash_key: bool = False) -> bool:
|
|
361
400
|
match node:
|
|
362
|
-
case ast.Constant()
|
|
401
|
+
case ast.Constant():
|
|
363
402
|
return True
|
|
364
|
-
case ast.
|
|
365
|
-
|
|
403
|
+
case ast.Name():
|
|
404
|
+
binding = provenance.binding(node.id, node)
|
|
405
|
+
if binding is None:
|
|
406
|
+
return node.id in _EXCEPTION_TYPES and provenance.context.imports.builtin_is_unshadowed(node.id)
|
|
407
|
+
if not isinstance(binding, ast.Name) or provenance.scope(binding) is not provenance.scope(node):
|
|
408
|
+
return False
|
|
409
|
+
# Stable, already executed scalar assignments prove both initialization and native hashing.
|
|
410
|
+
return isinstance(provenance.assignment_value(binding, node), ast.Constant)
|
|
411
|
+
case ast.List() | ast.Tuple():
|
|
412
|
+
return (not hash_key or isinstance(node, ast.Tuple)) and all(
|
|
413
|
+
_is_safe_value(element, provenance, hash_key=hash_key) for element in node.elts
|
|
414
|
+
)
|
|
415
|
+
case ast.Set():
|
|
416
|
+
return not hash_key and all(_is_safe_value(element, provenance, hash_key=True) for element in node.elts)
|
|
366
417
|
case ast.Dict():
|
|
367
|
-
return
|
|
368
|
-
|
|
418
|
+
return (
|
|
419
|
+
not hash_key
|
|
420
|
+
and all(key is not None and _is_safe_value(key, provenance, hash_key=True) for key in node.keys)
|
|
421
|
+
and all(_is_safe_value(value, provenance) for value in node.values)
|
|
369
422
|
)
|
|
370
423
|
case _:
|
|
371
424
|
return False
|
|
372
425
|
|
|
373
426
|
|
|
427
|
+
def _inert_configuration(stmt: ast.stmt, target: str, provenance: ResourceProvenance) -> bool:
|
|
428
|
+
if not isinstance(stmt, ast.Assign | ast.AnnAssign):
|
|
429
|
+
return False
|
|
430
|
+
value = stmt.value
|
|
431
|
+
assigned = stmt.targets[0] if isinstance(stmt, ast.Assign) else stmt.target
|
|
432
|
+
if isinstance(assigned, ast.Name) and isinstance(value, ast.Call):
|
|
433
|
+
attribute = target.rsplit(".", 1)[1]
|
|
434
|
+
value = next((keyword.value for keyword in value.keywords if keyword.arg == attribute), None)
|
|
435
|
+
return value is not None and _is_safe_value(value, provenance)
|
|
436
|
+
|
|
437
|
+
|
|
374
438
|
def _not_called_assertions(statements: list[ast.stmt], origins: frozenset[str]) -> dict[str, int]:
|
|
375
439
|
found: dict[str, int] = {}
|
|
376
440
|
for stmt in statements:
|
|
@@ -437,15 +501,24 @@ def _touches(path: str, other: str) -> bool:
|
|
|
437
501
|
|
|
438
502
|
|
|
439
503
|
def _overwritten_block_setups(
|
|
440
|
-
block: list[ast.stmt],
|
|
504
|
+
block: list[ast.stmt],
|
|
505
|
+
origins: frozenset[str],
|
|
506
|
+
provenance: ResourceProvenance,
|
|
507
|
+
*,
|
|
508
|
+
has_mocker_fixture: bool,
|
|
441
509
|
) -> Iterator[_Finding]:
|
|
442
510
|
pending: dict[str, ast.stmt] = {}
|
|
443
511
|
for stmt in block:
|
|
444
|
-
target = _config_target(stmt, origins)
|
|
512
|
+
target = _config_target(stmt, origins, provenance)
|
|
445
513
|
if target is None:
|
|
446
|
-
pending = _pending_after_statement(
|
|
514
|
+
pending = _pending_after_statement(
|
|
515
|
+
stmt, origins, pending, provenance, has_mocker_fixture=has_mocker_fixture
|
|
516
|
+
)
|
|
447
517
|
continue
|
|
448
|
-
if
|
|
518
|
+
if not _inert_configuration(stmt, target, provenance):
|
|
519
|
+
pending.clear()
|
|
520
|
+
continue
|
|
521
|
+
if _statement_observes_pending(stmt, pending, provenance):
|
|
449
522
|
pending.clear()
|
|
450
523
|
previous = pending.get(target)
|
|
451
524
|
if previous is not None:
|
|
@@ -453,14 +526,18 @@ def _overwritten_block_setups(
|
|
|
453
526
|
previous,
|
|
454
527
|
(
|
|
455
528
|
f"`{target}` is set here and overwritten on line {stmt.lineno} with nothing in "
|
|
456
|
-
"between that could call the mock, so this value is never used.
|
|
457
|
-
|
|
529
|
+
"between that could call the mock, so this value is never used. "
|
|
530
|
+
+ (
|
|
531
|
+
"Remove the overwritten constructor keyword, retaining the mock constructor and binding"
|
|
532
|
+
if isinstance(previous, ast.Assign | ast.AnnAssign) and isinstance(previous.value, ast.Call)
|
|
533
|
+
else "Delete the dead setup, or move the code under test between the two configurations"
|
|
534
|
+
)
|
|
458
535
|
),
|
|
459
536
|
)
|
|
460
537
|
pending[target] = stmt
|
|
461
538
|
|
|
462
539
|
|
|
463
|
-
def _statement_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt]) -> bool:
|
|
540
|
+
def _statement_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt], provenance: ResourceProvenance) -> bool:
|
|
464
541
|
if not pending:
|
|
465
542
|
return False
|
|
466
543
|
value = stmt.value if isinstance(stmt, (ast.Assign, ast.AnnAssign)) else None
|
|
@@ -472,7 +549,7 @@ def _statement_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt]) ->
|
|
|
472
549
|
path = _dotted(node)
|
|
473
550
|
if path is not None and any(_touches(path, target.rsplit(".", 1)[0]) for target in pending):
|
|
474
551
|
return True
|
|
475
|
-
return not _is_safe_value(value)
|
|
552
|
+
return not _is_safe_value(value, provenance)
|
|
476
553
|
|
|
477
554
|
|
|
478
555
|
def _unused_asserted_setup(
|
|
@@ -484,7 +561,7 @@ def _unused_asserted_setup(
|
|
|
484
561
|
stmt,
|
|
485
562
|
(
|
|
486
563
|
f"`{target}` is configured here, but the test ends by proving `{asserted}` was never "
|
|
487
|
-
"called, so its behavior cannot affect the test. Delete the
|
|
564
|
+
"called, so its behavior cannot affect the test. Delete this inert configuration; retain the mock constructor and binding"
|
|
488
565
|
),
|
|
489
566
|
)
|
|
490
567
|
break
|
|
@@ -492,32 +569,34 @@ def _unused_asserted_setup(
|
|
|
492
569
|
|
|
493
570
|
def _pending_after_statement(
|
|
494
571
|
stmt: ast.stmt,
|
|
495
|
-
imports: ImportIndex,
|
|
496
572
|
origins: frozenset[str],
|
|
497
573
|
pending: dict[str, ast.stmt],
|
|
574
|
+
provenance: ResourceProvenance,
|
|
498
575
|
*,
|
|
499
576
|
has_mocker_fixture: bool,
|
|
500
577
|
) -> dict[str, ast.stmt]:
|
|
501
|
-
initial = _initial_mock_configs(stmt,
|
|
578
|
+
initial = _initial_mock_configs(stmt, provenance, has_mocker_fixture=has_mocker_fixture)
|
|
502
579
|
if initial and initial[0].partition(".")[0] in origins:
|
|
503
|
-
if _constructor_observes_pending(stmt, pending):
|
|
580
|
+
if _constructor_observes_pending(stmt, pending, provenance):
|
|
504
581
|
pending.clear()
|
|
505
582
|
configured_name = initial[0].partition(".")[0]
|
|
506
583
|
pending = {target: setup for target, setup in pending.items() if not target.startswith(f"{configured_name}.")}
|
|
507
|
-
pending.update(
|
|
584
|
+
pending.update(
|
|
585
|
+
(configured, stmt) for configured in initial if _inert_configuration(stmt, configured, provenance)
|
|
586
|
+
)
|
|
508
587
|
return pending
|
|
509
|
-
if not _is_inert(stmt):
|
|
588
|
+
if not _is_inert(stmt, provenance):
|
|
510
589
|
pending.clear()
|
|
511
590
|
return pending
|
|
512
591
|
|
|
513
592
|
|
|
514
|
-
def _constructor_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt]) -> bool:
|
|
593
|
+
def _constructor_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt], provenance: ResourceProvenance) -> bool:
|
|
515
594
|
value = stmt.value if isinstance(stmt, (ast.Assign, ast.AnnAssign)) else None
|
|
516
595
|
if not isinstance(value, ast.Call):
|
|
517
596
|
return True
|
|
518
597
|
expressions = (*value.args, *(keyword.value for keyword in value.keywords))
|
|
519
598
|
return any(
|
|
520
|
-
not _is_safe_value(expression)
|
|
599
|
+
not _is_safe_value(expression, provenance)
|
|
521
600
|
or any(
|
|
522
601
|
isinstance(node, (ast.Name, ast.Attribute))
|
|
523
602
|
and isinstance(node.ctx, ast.Load)
|
|
@@ -529,7 +608,7 @@ def _constructor_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt])
|
|
|
529
608
|
)
|
|
530
609
|
|
|
531
610
|
|
|
532
|
-
def _is_inert(stmt: ast.stmt) -> bool:
|
|
611
|
+
def _is_inert(stmt: ast.stmt, provenance: ResourceProvenance) -> bool:
|
|
533
612
|
if isinstance(stmt, (ast.Pass, ast.Import, ast.ImportFrom)):
|
|
534
613
|
return True
|
|
535
614
|
if isinstance(stmt, ast.Expr):
|
|
@@ -539,10 +618,12 @@ def _is_inert(stmt: ast.stmt) -> bool:
|
|
|
539
618
|
if not all(isinstance(target, ast.Name) for target in stmt.targets):
|
|
540
619
|
# `obj.attr = 1` can hit a property setter, `d[k] = 1` a `__setitem__`.
|
|
541
620
|
return False
|
|
542
|
-
return _is_safe_value(stmt.value)
|
|
621
|
+
return _is_safe_value(stmt.value, provenance)
|
|
543
622
|
|
|
544
623
|
|
|
545
|
-
def _initial_mock_configs(
|
|
624
|
+
def _initial_mock_configs(
|
|
625
|
+
stmt: ast.stmt, provenance: ResourceProvenance, *, has_mocker_fixture: bool
|
|
626
|
+
) -> tuple[str, ...]:
|
|
546
627
|
match stmt:
|
|
547
628
|
case ast.Assign(targets=[ast.Name(id=name)], value=ast.Call() as call):
|
|
548
629
|
pass
|
|
@@ -550,6 +631,6 @@ def _initial_mock_configs(stmt: ast.stmt, imports: ImportIndex, *, has_mocker_fi
|
|
|
550
631
|
pass
|
|
551
632
|
case _:
|
|
552
633
|
return ()
|
|
553
|
-
if not _is_mock_configurator(call
|
|
634
|
+
if not _is_mock_configurator(call, provenance, has_mocker_fixture=has_mocker_fixture):
|
|
554
635
|
return ()
|
|
555
636
|
return tuple(f"{name}.{keyword.arg}" for keyword in call.keywords if keyword.arg in _CONFIG_ATTRS)
|
|
File without changes
|