sarj-python-lint 1.16.1__tar.gz → 1.16.3__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.16.1 → sarj_python_lint-1.16.3}/PKG-INFO +1 -1
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/pyproject.toml +1 -1
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +123 -21
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/unused_test_factory_option.py +124 -11
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/.gitignore +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/LICENSE +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/README.md +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/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.16.
|
|
3
|
+
Version: 1.16.3
|
|
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/
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import replace
|
|
4
5
|
from pathlib import PurePosixPath
|
|
6
|
+
from types import MappingProxyType
|
|
5
7
|
from typing import TYPE_CHECKING, ClassVar, final
|
|
6
8
|
|
|
7
9
|
from sarj_python_lint.rule_base import (
|
|
@@ -18,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
18
20
|
)
|
|
19
21
|
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
|
|
20
22
|
from sarj_python_lint.rules._ast_index import walk
|
|
23
|
+
from sarj_python_lint.rules.no_hidden_constructor_fallback import LocalBindingCollector, scope_bindings
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
if TYPE_CHECKING:
|
|
@@ -62,7 +65,7 @@ class NoBroadKeywordCapture(Rule):
|
|
|
62
65
|
"Reports missing, Any- or object-valued keyword captures whether discarded, read or forwarded.",
|
|
63
66
|
"Precise or unresolved decorated signature-declaration families and functions with unresolved decorator effects are excluded; known classmethod/staticmethod/abstractmethod/final/override declarations remain eligible.",
|
|
64
67
|
"Homogeneous keyword types constrain values but allow arbitrary names; ParamSpec preserves only its target's existing contract.",
|
|
65
|
-
"Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports and shadowed annotation provenance are conservative.",
|
|
68
|
+
"Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports, conflicting imports, actual global or definition-time writes and shadowed annotation provenance are conservative. Identical imports and read-only global declarations remain eligible.",
|
|
66
69
|
"Cross-file aliases, external stubs, generated signatures and runtime reflection are not resolved; transparent annotation metadata and ambiguous decorated declaration families can cause conservative false negatives.",
|
|
67
70
|
"No autofix can infer the intended contract. Dynamic payload/formatting APIs can intentionally need an exact-code exception.",
|
|
68
71
|
),
|
|
@@ -165,7 +168,8 @@ class NoBroadKeywordCapture(Rule):
|
|
|
165
168
|
tree = context.tree
|
|
166
169
|
if tree is None:
|
|
167
170
|
return []
|
|
168
|
-
semantics =
|
|
171
|
+
semantics = _stable_annotation_semantics(context, tree)
|
|
172
|
+
mutated_members = _mutated_annotation_members(context, semantics)
|
|
169
173
|
overloads: set[ast.FunctionDef | ast.AsyncFunctionDef] | None = None
|
|
170
174
|
findings: list[Diagnostic] = []
|
|
171
175
|
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
|
|
@@ -174,12 +178,12 @@ class NoBroadKeywordCapture(Rule):
|
|
|
174
178
|
continue
|
|
175
179
|
if not _is_broad(argument.annotation, semantics):
|
|
176
180
|
continue
|
|
177
|
-
if _uncertain_annotation(argument.annotation, function, context, semantics):
|
|
181
|
+
if _uncertain_annotation(argument.annotation, function, context, semantics, mutated_members):
|
|
178
182
|
continue
|
|
179
|
-
if _unknown_decorator(function, context, semantics):
|
|
183
|
+
if _unknown_decorator(function, context, semantics, mutated_members):
|
|
180
184
|
continue
|
|
181
185
|
if overloads is None:
|
|
182
|
-
overloads = _overload_implementations(context, semantics)
|
|
186
|
+
overloads = _overload_implementations(context, semantics, mutated_members)
|
|
183
187
|
if function in overloads:
|
|
184
188
|
continue
|
|
185
189
|
if is_suppressed(context.source_lines, argument.lineno, self.code) or is_suppressed(
|
|
@@ -202,6 +206,90 @@ class NoBroadKeywordCapture(Rule):
|
|
|
202
206
|
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
203
207
|
|
|
204
208
|
|
|
209
|
+
def _stable_annotation_semantics(context: PythonFileContext, tree: ast.Module) -> AnnotationSemantics:
|
|
210
|
+
semantics = AnnotationSemantics.from_tree(tree)
|
|
211
|
+
unstable = _conflicting_annotation_imports(context, semantics) | _annotation_binding_writes(context)
|
|
212
|
+
if not unstable:
|
|
213
|
+
return semantics
|
|
214
|
+
imports = replace(
|
|
215
|
+
semantics.imports,
|
|
216
|
+
bindings=MappingProxyType(
|
|
217
|
+
{name: target for name, target in semantics.imports.bindings.items() if name not in unstable}
|
|
218
|
+
),
|
|
219
|
+
shadowed_names=semantics.imports.shadowed_names | unstable,
|
|
220
|
+
)
|
|
221
|
+
return replace(
|
|
222
|
+
semantics,
|
|
223
|
+
imports=imports,
|
|
224
|
+
aliases=MappingProxyType({name: value for name, value in semantics.aliases.items() if name not in unstable}),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _conflicting_annotation_imports(context: PythonFileContext, semantics: AnnotationSemantics) -> set[str]:
|
|
229
|
+
conflicts: set[str] = set()
|
|
230
|
+
for statement in context.nodes(ast.Import, ast.ImportFrom):
|
|
231
|
+
if not isinstance(_lexical_scope(statement, context), ast.Module):
|
|
232
|
+
continue
|
|
233
|
+
for alias in statement.names:
|
|
234
|
+
name = alias.asname or alias.name.partition(".")[0]
|
|
235
|
+
binding = semantics.imports.bindings.get(name)
|
|
236
|
+
if binding is not None and _import_reference(statement, alias) != (binding.module, binding.symbol):
|
|
237
|
+
conflicts.add(name)
|
|
238
|
+
return conflicts
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _import_reference(statement: ast.Import | ast.ImportFrom, alias: ast.alias) -> tuple[str, str | None] | None:
|
|
242
|
+
if isinstance(statement, ast.ImportFrom):
|
|
243
|
+
return (statement.module, alias.name) if not statement.level and statement.module is not None else None
|
|
244
|
+
return (alias.name if alias.asname else alias.name.partition(".")[0], None)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _annotation_binding_writes(context: PythonFileContext) -> set[str]:
|
|
248
|
+
writes: set[str] = set()
|
|
249
|
+
for owner in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda):
|
|
250
|
+
if isinstance(_lexical_scope(owner, context), ast.Module):
|
|
251
|
+
header = LocalBindingCollector()
|
|
252
|
+
header.visit(owner)
|
|
253
|
+
if not isinstance(owner, ast.Lambda):
|
|
254
|
+
header.names.discard(owner.name)
|
|
255
|
+
writes.update(header.names)
|
|
256
|
+
if not isinstance(owner, ast.Lambda):
|
|
257
|
+
body = LocalBindingCollector()
|
|
258
|
+
for statement in owner.body:
|
|
259
|
+
body.visit(statement)
|
|
260
|
+
writes.update(body.names & body.globals)
|
|
261
|
+
return writes
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _mutated_annotation_members(context: PythonFileContext, semantics: AnnotationSemantics) -> frozenset[str]:
|
|
265
|
+
members: set[str] = set()
|
|
266
|
+
for node in context.nodes(ast.Attribute):
|
|
267
|
+
if not isinstance(node.ctx, (ast.Store, ast.Del)) or _shadowed_annotation(node, node, context, semantics):
|
|
268
|
+
continue
|
|
269
|
+
qualified = semantics.imports.resolved_qualified_name(node)
|
|
270
|
+
if qualified is not None:
|
|
271
|
+
members.add(qualified)
|
|
272
|
+
return frozenset(members)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _uses_mutated_member(annotation: ast.expr, semantics: AnnotationSemantics, mutated_members: frozenset[str]) -> bool:
|
|
276
|
+
if not mutated_members:
|
|
277
|
+
return False
|
|
278
|
+
pending = [annotation]
|
|
279
|
+
seen: set[str] = set()
|
|
280
|
+
while pending:
|
|
281
|
+
parsed = semantics.parse(pending.pop())
|
|
282
|
+
if parsed is None:
|
|
283
|
+
continue
|
|
284
|
+
for node in walk(parsed):
|
|
285
|
+
if isinstance(node, ast.Attribute) and semantics.imports.resolved_qualified_name(node) in mutated_members:
|
|
286
|
+
return True
|
|
287
|
+
if isinstance(node, ast.Name) and node.id in semantics.aliases and node.id not in seen:
|
|
288
|
+
seen.add(node.id)
|
|
289
|
+
pending.append(semantics.aliases[node.id])
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
|
|
205
293
|
def _is_broad(annotation: ast.expr | None, semantics: AnnotationSemantics) -> bool:
|
|
206
294
|
if annotation is None:
|
|
207
295
|
return True
|
|
@@ -215,29 +303,35 @@ def _is_broad(annotation: ast.expr | None, semantics: AnnotationSemantics) -> bo
|
|
|
215
303
|
|
|
216
304
|
def _shadowed_annotation(
|
|
217
305
|
annotation: ast.expr | None,
|
|
218
|
-
function: ast.
|
|
306
|
+
function: ast.AST,
|
|
219
307
|
context: PythonFileContext,
|
|
220
308
|
semantics: AnnotationSemantics,
|
|
221
309
|
) -> bool:
|
|
222
310
|
names = _uses_names(annotation, semantics)
|
|
223
|
-
if names & _type_parameter_names(
|
|
311
|
+
if isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and names & _type_parameter_names(
|
|
312
|
+
function
|
|
313
|
+
):
|
|
224
314
|
return True
|
|
225
315
|
parent = context.parents.get(function)
|
|
226
316
|
while parent is not None:
|
|
227
|
-
if isinstance(
|
|
228
|
-
parent
|
|
229
|
-
):
|
|
317
|
+
if isinstance(
|
|
318
|
+
parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
319
|
+
) and names & _scope_bindings(parent):
|
|
230
320
|
return True
|
|
231
321
|
parent = context.parents.get(parent)
|
|
232
322
|
return False
|
|
233
323
|
|
|
234
324
|
|
|
235
|
-
def _scope_bindings(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> set[str]:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
325
|
+
def _scope_bindings(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda) -> set[str]:
|
|
326
|
+
if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
|
|
327
|
+
bound = set(scope_bindings(owner))
|
|
328
|
+
else:
|
|
329
|
+
collector = LocalBindingCollector()
|
|
330
|
+
for statement in owner.body:
|
|
331
|
+
collector.visit(statement)
|
|
332
|
+
bound = collector.names - collector.globals
|
|
333
|
+
if not isinstance(owner, ast.Lambda):
|
|
334
|
+
bound.update(_type_parameter_names(owner))
|
|
241
335
|
return bound
|
|
242
336
|
|
|
243
337
|
|
|
@@ -250,10 +344,11 @@ def _uncertain_annotation(
|
|
|
250
344
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
251
345
|
context: PythonFileContext,
|
|
252
346
|
semantics: AnnotationSemantics,
|
|
347
|
+
mutated_members: frozenset[str],
|
|
253
348
|
) -> bool:
|
|
254
349
|
if annotation is None:
|
|
255
350
|
return False
|
|
256
|
-
if _has_wildcard_import(context):
|
|
351
|
+
if _has_wildcard_import(context) or _uses_mutated_member(annotation, semantics, mutated_members):
|
|
257
352
|
return True
|
|
258
353
|
if context.tree is not None:
|
|
259
354
|
unknown = scope_bound_names(context.tree.body) - set(semantics.imports.bindings) - set(semantics.aliases)
|
|
@@ -280,8 +375,9 @@ def _known_decorator(
|
|
|
280
375
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
281
376
|
context: PythonFileContext,
|
|
282
377
|
semantics: AnnotationSemantics,
|
|
378
|
+
mutated_members: frozenset[str],
|
|
283
379
|
) -> str | None:
|
|
284
|
-
if _has_wildcard_import(context):
|
|
380
|
+
if _has_wildcard_import(context) or _uses_mutated_member(decorator, semantics, mutated_members):
|
|
285
381
|
return None
|
|
286
382
|
if _shadowed_annotation(decorator, function, context, semantics):
|
|
287
383
|
return None
|
|
@@ -297,9 +393,10 @@ def _unknown_decorator(
|
|
|
297
393
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
298
394
|
context: PythonFileContext,
|
|
299
395
|
semantics: AnnotationSemantics,
|
|
396
|
+
mutated_members: frozenset[str],
|
|
300
397
|
) -> bool:
|
|
301
398
|
return any(
|
|
302
|
-
_known_decorator(decorator, function, context, semantics) not in _KNOWN_DECORATORS
|
|
399
|
+
_known_decorator(decorator, function, context, semantics, mutated_members) not in _KNOWN_DECORATORS
|
|
303
400
|
for decorator in function.decorator_list
|
|
304
401
|
)
|
|
305
402
|
|
|
@@ -307,6 +404,7 @@ def _unknown_decorator(
|
|
|
307
404
|
def _overload_implementations(
|
|
308
405
|
context: PythonFileContext,
|
|
309
406
|
semantics: AnnotationSemantics,
|
|
407
|
+
mutated_members: frozenset[str],
|
|
310
408
|
) -> set[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
311
409
|
families: dict[tuple[ast.AST | None, str], list[ast.FunctionDef | ast.AsyncFunctionDef]] = {}
|
|
312
410
|
implementations: set[ast.FunctionDef | ast.AsyncFunctionDef] = set()
|
|
@@ -315,7 +413,7 @@ def _overload_implementations(
|
|
|
315
413
|
scope = _lexical_scope(node, context)
|
|
316
414
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
317
415
|
key = scope, node.name
|
|
318
|
-
if _signature_declaration(node, context, semantics):
|
|
416
|
+
if _signature_declaration(node, context, semantics, mutated_members):
|
|
319
417
|
families.setdefault(key, []).append(node)
|
|
320
418
|
else:
|
|
321
419
|
family = families.pop(key, [])
|
|
@@ -356,6 +454,10 @@ def _signature_declaration(
|
|
|
356
454
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
357
455
|
context: PythonFileContext,
|
|
358
456
|
semantics: AnnotationSemantics,
|
|
457
|
+
mutated_members: frozenset[str],
|
|
359
458
|
) -> bool:
|
|
360
|
-
decorators = {
|
|
459
|
+
decorators = {
|
|
460
|
+
_known_decorator(decorator, function, context, semantics, mutated_members)
|
|
461
|
+
for decorator in function.decorator_list
|
|
462
|
+
}
|
|
361
463
|
return bool(decorators & _OVERLOAD_DECORATORS) or not decorators <= _KNOWN_DECORATORS
|
|
@@ -38,15 +38,17 @@ class UnusedTestFactoryOption(Rule):
|
|
|
38
38
|
code = "SARJ443"
|
|
39
39
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
40
40
|
default_level=Severity.WARNING,
|
|
41
|
-
summary="A
|
|
41
|
+
summary="A private test factory exposes an invariant literal or unexercised local callable option.",
|
|
42
42
|
rationale="Unused customization obscures the values that actually distinguish test scenarios.",
|
|
43
|
-
remediation="Consider keeping the invariant
|
|
43
|
+
remediation="Consider keeping the invariant literal or stable local callable in the construction instead of exposing an unexercised option. Preserve intentional dependency contracts and callable default binding timing.",
|
|
44
44
|
category=RuleCategory.TESTING,
|
|
45
45
|
autofix=AutofixPolicy.NONE,
|
|
46
46
|
limitations=(
|
|
47
|
-
"
|
|
48
|
-
"
|
|
47
|
+
"Literal findings consider only private _make_ and _build_ helpers nested directly inside test functions, with at least two known direct callers and straight-line construction bodies.",
|
|
48
|
+
"Callable findings consider module-level private helpers with the same construction shape and an earlier undecorated local function default, only when at least two known direct callers all omit that option.",
|
|
49
|
+
"Conftest callable factories, explicit callable arguments, exports, decorators, escaping references, reflection, unpacking, rebinding, shadowing and ambiguous mutation or patch targets are excluded.",
|
|
49
50
|
"Same-name declarations elsewhere in the file conservatively exclude a helper. No autofix: this warning identifies unexercised customization, not an invalid callable contract.",
|
|
51
|
+
"Callable findings describe known callers in this file; cross-module and dynamic callers are not inferred. Retain intentional extension points with a reasoned exception, preserving definition-time default capture.",
|
|
50
52
|
),
|
|
51
53
|
examples=(
|
|
52
54
|
RuleExample(
|
|
@@ -63,6 +65,40 @@ class UnusedTestFactoryOption(Rule):
|
|
|
63
65
|
expected_count=1,
|
|
64
66
|
public=True,
|
|
65
67
|
),
|
|
68
|
+
RuleExample(
|
|
69
|
+
example_id="unused-local-callable-option",
|
|
70
|
+
scenario="callable-default",
|
|
71
|
+
title="Known callers do not replace a stable local dependency",
|
|
72
|
+
outcome=ExampleOutcome.MATCH,
|
|
73
|
+
files=(
|
|
74
|
+
ExampleFile.python(
|
|
75
|
+
"tests/test_widget.py",
|
|
76
|
+
"def _read():\n return 'ready'\n"
|
|
77
|
+
"def _make_widget(*, read=_read):\n return Widget(read=read)\n"
|
|
78
|
+
"_make_widget()\n_make_widget()\n",
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
focus_path=PurePosixPath("tests/test_widget.py"),
|
|
82
|
+
expected_count=1,
|
|
83
|
+
public=True,
|
|
84
|
+
),
|
|
85
|
+
RuleExample(
|
|
86
|
+
example_id="exercised-local-callable-option",
|
|
87
|
+
scenario="callable-default",
|
|
88
|
+
title="An explicit callback preserves the dependency contract",
|
|
89
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
90
|
+
files=(
|
|
91
|
+
ExampleFile.python(
|
|
92
|
+
"tests/test_widget.py",
|
|
93
|
+
"def _read():\n return 'ready'\n"
|
|
94
|
+
"def _make_widget(*, read=_read):\n return Widget(read=read)\n"
|
|
95
|
+
"_make_widget()\n_make_widget(read=_read)\n",
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
focus_path=PurePosixPath("tests/test_widget.py"),
|
|
99
|
+
expected_count=0,
|
|
100
|
+
public=True,
|
|
101
|
+
),
|
|
66
102
|
RuleExample(
|
|
67
103
|
example_id="exercised-size-option",
|
|
68
104
|
title="Keep customization that a caller exercises",
|
|
@@ -96,14 +132,15 @@ class UnusedTestFactoryOption(Rule):
|
|
|
96
132
|
lines = context.source_lines
|
|
97
133
|
findings: list[Diagnostic] = []
|
|
98
134
|
for function in context.nodes(ast.FunctionDef):
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
not _is_factory(function)
|
|
102
|
-
or not isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
103
|
-
or not owner.name.startswith("test_")
|
|
104
|
-
):
|
|
135
|
+
if not _is_factory(function):
|
|
105
136
|
continue
|
|
106
|
-
|
|
137
|
+
owner = context.parents.get(function)
|
|
138
|
+
if isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef) and owner.name.startswith("test_"):
|
|
139
|
+
findings.extend(
|
|
140
|
+
_factory_findings(tree, function, path, lines, self.code, node_index=context.node_index)
|
|
141
|
+
)
|
|
142
|
+
elif owner is tree and path.name != "conftest.py":
|
|
143
|
+
findings.extend(_callable_factory_findings(context, function, self.code))
|
|
107
144
|
return sorted(findings, key=lambda item: (item.line, item.col))
|
|
108
145
|
|
|
109
146
|
|
|
@@ -280,3 +317,79 @@ def _factory_options(function: ast.FunctionDef) -> list[tuple[ast.arg, ast.expr]
|
|
|
280
317
|
]
|
|
281
318
|
used = {node.id for node in nodes(function, ast.Name) if isinstance(node.ctx, ast.Load)}
|
|
282
319
|
return [(argument, default) for argument, default in defaults if argument.arg in used and default is not None]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _callable_factory_findings(context: PythonFileContext, function: ast.FunctionDef, code: str) -> list[Diagnostic]:
|
|
323
|
+
tree = context.tree
|
|
324
|
+
if tree is None:
|
|
325
|
+
return []
|
|
326
|
+
options = [(argument, default) for argument, default in _factory_options(function) if isinstance(default, ast.Name)]
|
|
327
|
+
if not options:
|
|
328
|
+
return []
|
|
329
|
+
calls = _direct_calls(tree, function, node_index=context.node_index)
|
|
330
|
+
if len(calls) < _MIN_INVARIANT_CALLS:
|
|
331
|
+
return []
|
|
332
|
+
bound = _bound_calls(function, calls)
|
|
333
|
+
if bound is None:
|
|
334
|
+
return []
|
|
335
|
+
supplied = {name for arguments in bound for name in arguments}
|
|
336
|
+
rebound = {node.id for node in nodes(function, ast.Name) if not isinstance(node.ctx, ast.Load)}
|
|
337
|
+
findings: list[Diagnostic] = []
|
|
338
|
+
for argument, default in options:
|
|
339
|
+
if (
|
|
340
|
+
argument.arg in supplied | rebound
|
|
341
|
+
or not _is_stable_local_callable(context, function, default.id)
|
|
342
|
+
or is_suppressed(context.source_lines, argument.lineno, code)
|
|
343
|
+
):
|
|
344
|
+
continue
|
|
345
|
+
findings.append(
|
|
346
|
+
Diagnostic(
|
|
347
|
+
path=context.path,
|
|
348
|
+
line=argument.lineno,
|
|
349
|
+
col=argument.col_offset + 1,
|
|
350
|
+
code=code,
|
|
351
|
+
severity=Severity.WARNING,
|
|
352
|
+
message=(
|
|
353
|
+
f"No known direct caller in this file supplies `{function.name}.{argument.arg}`; consider "
|
|
354
|
+
f"using the stable local callable `{default.id}` internally. Preserve default binding timing "
|
|
355
|
+
"and retain the option if external callers or an intentional test contract need it."
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
return findings
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _is_stable_local_callable(context: PythonFileContext, factory: ast.FunctionDef, name: str) -> bool:
|
|
363
|
+
tree = context.tree
|
|
364
|
+
if tree is None or not any(
|
|
365
|
+
isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
366
|
+
and node.name == name
|
|
367
|
+
and node.lineno < factory.lineno
|
|
368
|
+
and not node.decorator_list
|
|
369
|
+
for node in tree.body
|
|
370
|
+
):
|
|
371
|
+
return False
|
|
372
|
+
if not _has_unambiguous_factory_name(tree, name, node_index=context.node_index):
|
|
373
|
+
return False
|
|
374
|
+
return not (
|
|
375
|
+
any(node.id == name and not isinstance(node.ctx, ast.Load) for node in context.nodes(ast.Name))
|
|
376
|
+
or any(
|
|
377
|
+
not isinstance(node.ctx, ast.Load) and any(child.id == name for child in nodes(node.value, ast.Name))
|
|
378
|
+
for node in context.nodes(ast.Attribute, ast.Subscript)
|
|
379
|
+
)
|
|
380
|
+
or any(
|
|
381
|
+
isinstance(node.value, str) and (name in node.value.split(".") or factory.name in node.value.split("."))
|
|
382
|
+
for node in context.nodes(ast.Constant)
|
|
383
|
+
)
|
|
384
|
+
or _provider_escapes(context, name)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _provider_escapes(context: PythonFileContext, name: str) -> bool:
|
|
389
|
+
for node in context.nodes(ast.Name):
|
|
390
|
+
if node.id != name or not isinstance(node.ctx, ast.Load):
|
|
391
|
+
continue
|
|
392
|
+
parent = context.parents.get(node)
|
|
393
|
+
if not (isinstance(parent, ast.arguments) or (isinstance(parent, ast.Call) and parent.func is node)):
|
|
394
|
+
return True
|
|
395
|
+
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_dunder_all.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|