sarj-python-lint 1.10.1__tar.gz → 1.13.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.10.1 → sarj_python_lint-1.13.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_registry.py +4 -0
- sarj_python_lint-1.13.0/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +205 -0
- sarj_python_lint-1.13.0/src/sarj_python_lint/rules/require_precise_factory_signature.py +252 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +94 -11
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/.gitignore +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/LICENSE +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/README.md +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.10.1 → sarj_python_lint-1.13.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.13.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/
|
|
@@ -79,6 +79,7 @@ from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
|
|
|
79
79
|
from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
|
|
80
80
|
from sarj_python_lint.rules.no_select_star import NoSelectStar
|
|
81
81
|
from sarj_python_lint.rules.no_service_behavior_in_settings import NoServiceBehaviorInSettings
|
|
82
|
+
from sarj_python_lint.rules.no_shallow_container_type_guard import NoShallowContainerTypeGuard
|
|
82
83
|
from sarj_python_lint.rules.no_string_concat_in_loop import NoStringConcatInLoop
|
|
83
84
|
from sarj_python_lint.rules.no_swallowed_asyncio_cancellation import NoSwallowedAsyncioCancellation
|
|
84
85
|
from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
|
|
@@ -164,6 +165,7 @@ from sarj_python_lint.rules.require_nodecode_for_splitting_settings_field import
|
|
|
164
165
|
RequireNoDecodeForSplittingSettingsField,
|
|
165
166
|
)
|
|
166
167
|
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
168
|
+
from sarj_python_lint.rules.require_precise_factory_signature import RequirePreciseFactorySignature
|
|
167
169
|
from sarj_python_lint.rules.require_public_dependency_contract import RequirePublicDependencyContract
|
|
168
170
|
from sarj_python_lint.rules.require_pydantic_for_external_json import (
|
|
169
171
|
RequirePydanticForExternalJson,
|
|
@@ -199,6 +201,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
199
201
|
RequireExplicitContractImplementation.id: RequireExplicitContractImplementation,
|
|
200
202
|
DiscourageNullableConstructorParameters.id: DiscourageNullableConstructorParameters,
|
|
201
203
|
RequirePublicDependencyContract.id: RequirePublicDependencyContract,
|
|
204
|
+
RequirePreciseFactorySignature.id: RequirePreciseFactorySignature,
|
|
202
205
|
RequireExplicitServiceContract.id: RequireExplicitServiceContract,
|
|
203
206
|
AsyncCleanupRegisteredSynchronously.id: AsyncCleanupRegisteredSynchronously,
|
|
204
207
|
SubprocessKillWithoutReap.id: SubprocessKillWithoutReap,
|
|
@@ -260,6 +263,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
260
263
|
NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
|
|
261
264
|
NoInjectedModuleLoader.id: NoInjectedModuleLoader,
|
|
262
265
|
NoNullableDependencyFallback.id: NoNullableDependencyFallback,
|
|
266
|
+
NoShallowContainerTypeGuard.id: NoShallowContainerTypeGuard,
|
|
263
267
|
NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
|
|
264
268
|
NoInvalidArgumentNameSuppression.id: NoInvalidArgumentNameSuppression,
|
|
265
269
|
NoFastapiOnEvent.id: NoFastapiOnEvent,
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._imports import TYPING_SOURCES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
24
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_CONTAINERS = frozenset({"dict", "list", "set"})
|
|
28
|
+
_MAPPING_SLOTS = 2
|
|
29
|
+
_SLOTS = frozenset({"object", "str", "int", "bool", "float", "bytes"})
|
|
30
|
+
_BUILTINS = _SLOTS | _CONTAINERS | {"isinstance"}
|
|
31
|
+
_MUTATION_TARGETS = {f"builtins.{symbol}" for symbol in _BUILTINS} | {
|
|
32
|
+
"typing.TypeGuard",
|
|
33
|
+
"typing.TypeIs",
|
|
34
|
+
"typing_extensions.TypeGuard",
|
|
35
|
+
"typing_extensions.TypeIs",
|
|
36
|
+
}
|
|
37
|
+
_BAD = "from typing import TypeGuard\ndef is_mapping(value: object) -> TypeGuard[dict[str, object]]:\n return isinstance(value, dict)\n"
|
|
38
|
+
_GOOD = _BAD.replace("dict[str, object]", "dict[object, object]")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@final
|
|
42
|
+
class NoShallowContainerTypeGuard(Rule):
|
|
43
|
+
id = "no-shallow-container-type-guard"
|
|
44
|
+
code = "SARJ472"
|
|
45
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
46
|
+
default_level=Severity.WARNING,
|
|
47
|
+
summary="Do not claim typed container contents from a container-only type guard.",
|
|
48
|
+
rationale="TypeGuard and TypeIs let callers trust their declared narrowing. Checking only dict, list, or set does not validate keys, values, or elements, so callers can perform operations that fail at runtime.",
|
|
49
|
+
remediation="Use object for unchecked container slots, or validate their contents when the narrower contract is required. Keep constrained input types that already prove the contents.",
|
|
50
|
+
category=RuleCategory.CORRECTNESS,
|
|
51
|
+
autofix=AutofixPolicy.NONE,
|
|
52
|
+
limitations=(
|
|
53
|
+
"Checks top-level, undecorated, non-generic synchronous functions with one positional parameter proven to be builtin object and a single return isinstance(parameter, builtin_container), optionally after a docstring.",
|
|
54
|
+
"The TypeGuard or TypeIs target must be a matching builtin dict, list, or set whose slots all resolve to object, str, int, bool, float, or bytes; at least one slot must be narrower than object.",
|
|
55
|
+
"Includes authored tests; generated and vendor source is excluded. String annotations, constrained input types, unions, Any, TypeVars, Protocols, assignment aliases, nested definitions, and content validators are not inferred.",
|
|
56
|
+
"Annotation imports include TYPE_CHECKING imports. Runtime imports use conservative file-wide shadow facts, so unrelated local rebinding can cause false negatives. Wildcard imports, explicit writes to relevant builtin or typing attributes, and exception, pattern or walrus bindings that shadow relevant names or imported aliases exclude the file; dynamic namespace mutation is not inferred.",
|
|
57
|
+
"No autofix: widening the return contract can require caller changes, while adding content validation changes behavior and cost.",
|
|
58
|
+
),
|
|
59
|
+
examples=(
|
|
60
|
+
RuleExample(
|
|
61
|
+
example_id="unchecked-dictionary-keys",
|
|
62
|
+
title="A container check does not prove string keys",
|
|
63
|
+
outcome=ExampleOutcome.MATCH,
|
|
64
|
+
files=(ExampleFile.python("app/values.py", _BAD),),
|
|
65
|
+
focus_path=PurePosixPath("app/values.py"),
|
|
66
|
+
expected_count=1,
|
|
67
|
+
public=True,
|
|
68
|
+
),
|
|
69
|
+
RuleExample(
|
|
70
|
+
example_id="honest-dictionary-keys",
|
|
71
|
+
title="Unchecked keys retain their object type",
|
|
72
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
73
|
+
files=(ExampleFile.python("app/values.py", _GOOD),),
|
|
74
|
+
focus_path=PurePosixPath("app/values.py"),
|
|
75
|
+
expected_count=0,
|
|
76
|
+
public=True,
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
description = documentation.summary
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
84
|
+
if context.tree is None or context.generated or _ambiguous_symbols(context):
|
|
85
|
+
return []
|
|
86
|
+
findings: list[Diagnostic] = []
|
|
87
|
+
for function in context.tree.body:
|
|
88
|
+
if not isinstance(function, ast.FunctionDef):
|
|
89
|
+
continue
|
|
90
|
+
statement = _shallow_guard(function, context)
|
|
91
|
+
if statement is None or is_suppressed(context.source_lines, statement.lineno, self.code):
|
|
92
|
+
continue
|
|
93
|
+
findings.append(
|
|
94
|
+
Diagnostic(
|
|
95
|
+
path=context.path,
|
|
96
|
+
line=statement.lineno,
|
|
97
|
+
col=statement.col_offset + 1,
|
|
98
|
+
code=self.code,
|
|
99
|
+
message="Container-only isinstance check does not validate the declared container contents; use object for unchecked slots or validate the contents required by the return type.",
|
|
100
|
+
severity=Severity.WARNING,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
return findings
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _shallow_guard(function: ast.FunctionDef, context: PythonFileContext) -> ast.Return | None:
|
|
107
|
+
arguments = function.args
|
|
108
|
+
positional = (*arguments.posonlyargs, *arguments.args)
|
|
109
|
+
if function.decorator_list or function.type_params or len(positional) != 1:
|
|
110
|
+
return None
|
|
111
|
+
if arguments.kwonlyargs or arguments.vararg is not None or arguments.kwarg is not None:
|
|
112
|
+
return None
|
|
113
|
+
parameter = positional[0]
|
|
114
|
+
if _builtin_symbol(parameter.annotation, context.module_imports) != "object":
|
|
115
|
+
return None
|
|
116
|
+
name = _guarded_container(function.returns, context.module_imports)
|
|
117
|
+
if name is None:
|
|
118
|
+
return None
|
|
119
|
+
body = function.body
|
|
120
|
+
if (
|
|
121
|
+
body
|
|
122
|
+
and isinstance(body[0], ast.Expr)
|
|
123
|
+
and isinstance(body[0].value, ast.Constant)
|
|
124
|
+
and isinstance(body[0].value.value, str)
|
|
125
|
+
):
|
|
126
|
+
body = body[1:]
|
|
127
|
+
if len(body) != 1 or not isinstance(body[0], ast.Return):
|
|
128
|
+
return None
|
|
129
|
+
statement = body[0]
|
|
130
|
+
call = statement.value
|
|
131
|
+
if not isinstance(call, ast.Call) or call.keywords:
|
|
132
|
+
return None
|
|
133
|
+
match call.args:
|
|
134
|
+
case [ast.Name(id=guarded_name), checked_container] if (
|
|
135
|
+
guarded_name == parameter.arg
|
|
136
|
+
and _builtin_symbol(call.func, context.imports) == "isinstance"
|
|
137
|
+
and _builtin_symbol(checked_container, context.imports) == name
|
|
138
|
+
):
|
|
139
|
+
return statement
|
|
140
|
+
case _:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _guarded_container(annotation: ast.expr | None, imports: ImportIndex) -> str | None:
|
|
145
|
+
if not isinstance(annotation, ast.Subscript) or imports.resolved_symbol(
|
|
146
|
+
annotation.value, sources=TYPING_SOURCES
|
|
147
|
+
) not in {"TypeGuard", "TypeIs"}:
|
|
148
|
+
return None
|
|
149
|
+
container = annotation.slice
|
|
150
|
+
if not isinstance(container, ast.Subscript):
|
|
151
|
+
return None
|
|
152
|
+
name = _builtin_symbol(container.value, imports)
|
|
153
|
+
if name not in _CONTAINERS:
|
|
154
|
+
return None
|
|
155
|
+
slots = container.slice.elts if isinstance(container.slice, ast.Tuple) else [container.slice]
|
|
156
|
+
kinds = [_builtin_symbol(slot, imports) for slot in slots]
|
|
157
|
+
if (
|
|
158
|
+
len(kinds) != (_MAPPING_SLOTS if name == "dict" else 1)
|
|
159
|
+
or any(kind not in _SLOTS for kind in kinds)
|
|
160
|
+
or all(kind == "object" for kind in kinds)
|
|
161
|
+
):
|
|
162
|
+
return None
|
|
163
|
+
return name
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _builtin_symbol(node: ast.expr | None, imports: ImportIndex) -> str | None:
|
|
167
|
+
if node is None:
|
|
168
|
+
return None
|
|
169
|
+
symbol = imports.resolved_symbol(node, sources=frozenset({"builtins"}))
|
|
170
|
+
if symbol in _BUILTINS:
|
|
171
|
+
return symbol
|
|
172
|
+
if isinstance(node, ast.Name) and node.id in _BUILTINS and imports.builtin_is_unshadowed(node.id):
|
|
173
|
+
return node.id
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _ambiguous_symbols(context: PythonFileContext) -> bool:
|
|
178
|
+
sensitive_names = _BUILTINS | {
|
|
179
|
+
name
|
|
180
|
+
for name, target in context.module_imports.bindings.items()
|
|
181
|
+
if target.module in TYPING_SOURCES | {"builtins"}
|
|
182
|
+
and (target.symbol is None or target.symbol in _BUILTINS | {"TypeGuard", "TypeIs"})
|
|
183
|
+
}
|
|
184
|
+
for node in context.nodes(
|
|
185
|
+
ast.ImportFrom, ast.Attribute, ast.ExceptHandler, ast.MatchAs, ast.MatchStar, ast.MatchMapping, ast.NamedExpr
|
|
186
|
+
):
|
|
187
|
+
match node:
|
|
188
|
+
case ast.ImportFrom(names=aliases):
|
|
189
|
+
if any(alias.name == "*" for alias in aliases):
|
|
190
|
+
return True
|
|
191
|
+
case ast.Attribute(ctx=(ast.Store() | ast.Del())):
|
|
192
|
+
if context.module_imports.resolved_qualified_name(node) in _MUTATION_TARGETS:
|
|
193
|
+
return True
|
|
194
|
+
case (
|
|
195
|
+
ast.ExceptHandler(name=name)
|
|
196
|
+
| ast.MatchAs(name=name)
|
|
197
|
+
| ast.MatchStar(name=name)
|
|
198
|
+
| ast.MatchMapping(rest=name)
|
|
199
|
+
| ast.NamedExpr(target=ast.Name(id=name))
|
|
200
|
+
):
|
|
201
|
+
if name in sensitive_names:
|
|
202
|
+
return True
|
|
203
|
+
case _:
|
|
204
|
+
pass
|
|
205
|
+
return False
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
ProjectRule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
)
|
|
18
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
19
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_CALLABLE_ARITY = 2
|
|
27
|
+
|
|
28
|
+
_CALLABLES = frozenset({"typing.Callable", "collections.abc.Callable", "typing_extensions.Callable"})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@final
|
|
32
|
+
class RequirePreciseFactorySignature(ProjectRule):
|
|
33
|
+
id = "require-precise-factory-signature"
|
|
34
|
+
code = "SARJ473"
|
|
35
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
36
|
+
default_level=Severity.WARNING,
|
|
37
|
+
summary="Keep fixed keyword factory calls behind a precise callable signature.",
|
|
38
|
+
rationale="Callable[..., T] erases argument checking, so misspelled keywords and incompatible injected factories pass strict typing and can fail at runtime.",
|
|
39
|
+
remediation="Use an exact callable Protocol for substitutable factories. Use type[T] when this dependency genuinely requires a class, accounting for subclass constructor compatibility.",
|
|
40
|
+
category=RuleCategory.ARCHITECTURE,
|
|
41
|
+
autofix=AutofixPolicy.NONE,
|
|
42
|
+
limitations=(
|
|
43
|
+
"Only undecorated module-level functions with proven standard Callable[..., OwnedClass] parameters directly called with at least one named keyword are considered. Imported classes require scanned first-party declarations.",
|
|
44
|
+
"Tests, test support, generated code, unresolved/string/generic annotations, positional-only calls, unpacking, forwarding, nested captures, rebinding and ambiguous imports are excluded. Import capture checks are conservative across the file, so an unrelated local pattern, exception or walrus binding can prevent a warning.",
|
|
45
|
+
"A fixed call demonstrates signature erasure, not a current runtime defect. Genuine dynamic APIs can use a reasoned suppression. No autofix: the correct contract depends on whether classes or function factories are intended.",
|
|
46
|
+
),
|
|
47
|
+
examples=(
|
|
48
|
+
RuleExample(
|
|
49
|
+
example_id="erased-factory",
|
|
50
|
+
title="Keyword arguments escape checking",
|
|
51
|
+
outcome=ExampleOutcome.MATCH,
|
|
52
|
+
files=(
|
|
53
|
+
ExampleFile.python(
|
|
54
|
+
"app/service.py",
|
|
55
|
+
"from collections.abc import Callable\nclass Result: pass\ndef build(factory: Callable[..., Result]):\n return factory(size=4)\n",
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
59
|
+
expected_count=1,
|
|
60
|
+
public=True,
|
|
61
|
+
),
|
|
62
|
+
RuleExample(
|
|
63
|
+
example_id="exact-factory",
|
|
64
|
+
title="Factory declares its keyword contract",
|
|
65
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
66
|
+
files=(
|
|
67
|
+
ExampleFile.python(
|
|
68
|
+
"app/service.py",
|
|
69
|
+
"from typing import Protocol\nclass Result: pass\nclass Factory(Protocol):\n def __call__(self, *, size: int) -> Result: ...\ndef build(factory: Factory):\n return factory(size=4)\n",
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
73
|
+
expected_count=0,
|
|
74
|
+
public=True,
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
description = documentation.summary
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
82
|
+
tree = context.tree
|
|
83
|
+
if tree is None or context.generated or is_test_path(context.path) or is_test_support_path(context.path):
|
|
84
|
+
return []
|
|
85
|
+
if any(alias.name == "*" for node in context.nodes(ast.ImportFrom) for alias in node.names):
|
|
86
|
+
return []
|
|
87
|
+
findings: list[Diagnostic] = []
|
|
88
|
+
for function in tree.body:
|
|
89
|
+
if (
|
|
90
|
+
not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
91
|
+
or function.decorator_list
|
|
92
|
+
or function.type_params
|
|
93
|
+
):
|
|
94
|
+
continue
|
|
95
|
+
for argument in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs):
|
|
96
|
+
result = _factory_result(argument.annotation, context)
|
|
97
|
+
if (
|
|
98
|
+
result is None
|
|
99
|
+
or not _fixed_calls(function, argument.arg, context)
|
|
100
|
+
or not _owned_result(result, function, context)
|
|
101
|
+
):
|
|
102
|
+
continue
|
|
103
|
+
findings.append(
|
|
104
|
+
Diagnostic(
|
|
105
|
+
path=context.path,
|
|
106
|
+
line=argument.lineno,
|
|
107
|
+
col=argument.col_offset + 1,
|
|
108
|
+
code=self.code,
|
|
109
|
+
message=f"`{argument.arg}` is called with fixed keywords but Callable[..., T] erases its argument contract; use a precise callable signature.",
|
|
110
|
+
severity=Severity.WARNING,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _factory_result(annotation: ast.expr | None, context: PythonFileContext) -> ast.Name | ast.Attribute | None:
|
|
117
|
+
if (
|
|
118
|
+
not isinstance(annotation, ast.Subscript)
|
|
119
|
+
or context.module_imports.resolved_qualified_name(annotation.value) not in _CALLABLES
|
|
120
|
+
):
|
|
121
|
+
return None
|
|
122
|
+
if not isinstance(annotation.slice, ast.Tuple) or len(annotation.slice.elts) != _CALLABLE_ARITY:
|
|
123
|
+
return None
|
|
124
|
+
parameters, result = annotation.slice.elts
|
|
125
|
+
if (
|
|
126
|
+
not isinstance(parameters, ast.Constant)
|
|
127
|
+
or parameters.value is not Ellipsis
|
|
128
|
+
or not isinstance(result, (ast.Name, ast.Attribute))
|
|
129
|
+
):
|
|
130
|
+
return None
|
|
131
|
+
root = _root(annotation.value)
|
|
132
|
+
if (root is not None and _captured_import_root(root, context)) or any(
|
|
133
|
+
isinstance(node.ctx, (ast.Store, ast.Del)) and _root(node) == root for node in context.nodes(ast.Attribute)
|
|
134
|
+
):
|
|
135
|
+
return None
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _fixed_calls(function: ast.FunctionDef | ast.AsyncFunctionDef, name: str, context: PythonFileContext) -> bool:
|
|
140
|
+
contents = [node for statement in function.body for node in walk(statement)]
|
|
141
|
+
references = [node for node in contents if isinstance(node, ast.Name) and node.id == name]
|
|
142
|
+
return (
|
|
143
|
+
bool(references)
|
|
144
|
+
and not any(_binds_name(node, name) for node in contents)
|
|
145
|
+
and all(_direct_keyword_call(reference, function, context) for reference in references)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _direct_keyword_call(
|
|
150
|
+
reference: ast.Name, function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext
|
|
151
|
+
) -> bool:
|
|
152
|
+
call = context.parents.get(reference)
|
|
153
|
+
if not isinstance(call, ast.Call) or call.func is not reference or not call.keywords:
|
|
154
|
+
return False
|
|
155
|
+
if any(keyword.arg is None for keyword in call.keywords) or any(isinstance(arg, ast.Starred) for arg in call.args):
|
|
156
|
+
return False
|
|
157
|
+
parent = context.parents.get(call)
|
|
158
|
+
while parent is not None and parent is not function:
|
|
159
|
+
if isinstance(
|
|
160
|
+
parent,
|
|
161
|
+
(
|
|
162
|
+
ast.FunctionDef,
|
|
163
|
+
ast.AsyncFunctionDef,
|
|
164
|
+
ast.Lambda,
|
|
165
|
+
ast.ClassDef,
|
|
166
|
+
ast.ListComp,
|
|
167
|
+
ast.SetComp,
|
|
168
|
+
ast.DictComp,
|
|
169
|
+
ast.GeneratorExp,
|
|
170
|
+
),
|
|
171
|
+
):
|
|
172
|
+
return False
|
|
173
|
+
parent = context.parents.get(parent)
|
|
174
|
+
return parent is function
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _owned_result(
|
|
178
|
+
result: ast.Name | ast.Attribute, function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext
|
|
179
|
+
) -> bool:
|
|
180
|
+
tree = context.tree
|
|
181
|
+
if tree is None:
|
|
182
|
+
return False
|
|
183
|
+
root = _root(result)
|
|
184
|
+
if any(isinstance(node.ctx, (ast.Store, ast.Del)) and _root(node) == root for node in context.nodes(ast.Attribute)):
|
|
185
|
+
return False
|
|
186
|
+
local = next(
|
|
187
|
+
(
|
|
188
|
+
node
|
|
189
|
+
for node in tree.body
|
|
190
|
+
if isinstance(node, ast.ClassDef) and isinstance(result, ast.Name) and node.name == result.id
|
|
191
|
+
),
|
|
192
|
+
None,
|
|
193
|
+
)
|
|
194
|
+
if local is not None:
|
|
195
|
+
return local.lineno < function.lineno and _stable_class(tree, local.name)
|
|
196
|
+
if root not in context.module_imports.bindings or (root is not None and _captured_import_root(root, context)):
|
|
197
|
+
return False
|
|
198
|
+
project = context.session.project
|
|
199
|
+
if project is None or (unit := project.unit_or_source(context.path, context.source, tree)) is None:
|
|
200
|
+
return False
|
|
201
|
+
summary = project.class_for(unit, result)
|
|
202
|
+
if (
|
|
203
|
+
summary is None
|
|
204
|
+
or (declaration := project.source_unit(summary.symbol.module)) is None
|
|
205
|
+
or declaration.tree is None
|
|
206
|
+
):
|
|
207
|
+
return False
|
|
208
|
+
return not (
|
|
209
|
+
is_generated(declaration.path, declaration.source)
|
|
210
|
+
or is_test_path(declaration.path)
|
|
211
|
+
or is_test_support_path(declaration.path)
|
|
212
|
+
) and _stable_class(declaration.tree, summary.symbol.name)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _stable_class(tree: ast.Module, name: str) -> bool:
|
|
216
|
+
declarations = [node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == name]
|
|
217
|
+
if len(declarations) != 1 or declarations[0].decorator_list:
|
|
218
|
+
return False
|
|
219
|
+
return not any(_binds_name(node, name) for node in walk(tree) if node is not declarations[0])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _captured_import_root(name: str, context: PythonFileContext) -> bool:
|
|
223
|
+
return any(
|
|
224
|
+
_binds_name(node, name)
|
|
225
|
+
for node in context.nodes(ast.MatchAs, ast.MatchStar, ast.MatchMapping, ast.ExceptHandler)
|
|
226
|
+
) or any(node.target.id == name for node in context.nodes(ast.NamedExpr))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _binds_name(node: ast.AST, name: str) -> bool:
|
|
230
|
+
match node:
|
|
231
|
+
case (
|
|
232
|
+
ast.Name(id=bound, ctx=(ast.Store() | ast.Del()))
|
|
233
|
+
| ast.arg(arg=bound)
|
|
234
|
+
| ast.FunctionDef(name=bound)
|
|
235
|
+
| ast.AsyncFunctionDef(name=bound)
|
|
236
|
+
| ast.ClassDef(name=bound)
|
|
237
|
+
| ast.ExceptHandler(name=str() as bound)
|
|
238
|
+
| ast.MatchAs(name=str() as bound)
|
|
239
|
+
| ast.MatchStar(name=str() as bound)
|
|
240
|
+
| ast.MatchMapping(rest=str() as bound)
|
|
241
|
+
):
|
|
242
|
+
return bound == name
|
|
243
|
+
case ast.alias(name=imported, asname=alias):
|
|
244
|
+
return (alias or imported.split(".")[0]) == name
|
|
245
|
+
case _:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _root(node: ast.expr) -> str | None:
|
|
250
|
+
while isinstance(node, ast.Attribute):
|
|
251
|
+
node = node.value
|
|
252
|
+
return node.id if isinstance(node, ast.Name) else None
|
|
@@ -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.10.1 → sarj_python_lint-1.13.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|