sarj-python-lint 1.14.3__tar.gz → 1.14.4__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.14.3 → sarj_python_lint-1.14.4}/PKG-INFO +1 -1
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/pyproject.toml +1 -1
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_imports.py +15 -2
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +12 -1
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +2 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +154 -21
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_injected_module_loader.py +32 -1
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +16 -3
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +152 -24
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +37 -3
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +13 -5
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +13 -2
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +33 -9
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_port_for_service.py +71 -18
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_precise_factory_signature.py +22 -4
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/.gitignore +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/LICENSE +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/README.md +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.14.3 → sarj_python_lint-1.14.4}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.14.
|
|
3
|
+
Version: 1.14.4
|
|
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/
|
|
@@ -85,7 +85,14 @@ def _non_import_bindings(tree: ast.Module) -> set[str]:
|
|
|
85
85
|
names: set[str] = set()
|
|
86
86
|
for node in walk_ast(tree):
|
|
87
87
|
match node:
|
|
88
|
-
case
|
|
88
|
+
case (
|
|
89
|
+
ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
|
|
90
|
+
| ast.arg(arg=name)
|
|
91
|
+
| ast.ExceptHandler(name=str() as name)
|
|
92
|
+
| ast.MatchAs(name=str() as name)
|
|
93
|
+
| ast.MatchStar(name=str() as name)
|
|
94
|
+
| ast.MatchMapping(rest=str() as name)
|
|
95
|
+
):
|
|
89
96
|
names.add(name)
|
|
90
97
|
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
91
98
|
names.add(node.name)
|
|
@@ -142,7 +149,13 @@ def _collect_module_bindings(node: ast.AST, names: set[str]) -> None:
|
|
|
142
149
|
return
|
|
143
150
|
case ast.Lambda():
|
|
144
151
|
return
|
|
145
|
-
case
|
|
152
|
+
case (
|
|
153
|
+
ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
|
|
154
|
+
| ast.ExceptHandler(name=str() as name)
|
|
155
|
+
| ast.MatchAs(name=str() as name)
|
|
156
|
+
| ast.MatchStar(name=str() as name)
|
|
157
|
+
| ast.MatchMapping(rest=str() as name)
|
|
158
|
+
):
|
|
146
159
|
names.add(name)
|
|
147
160
|
case _:
|
|
148
161
|
pass
|
|
@@ -476,8 +476,19 @@ def _invalidates_reference(node: ast.AST, reference: _Reference, binding: ast.Na
|
|
|
476
476
|
match node:
|
|
477
477
|
case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
|
|
478
478
|
return node is not binding and name == reference[0]
|
|
479
|
-
case
|
|
479
|
+
case (
|
|
480
|
+
ast.arg(arg=name)
|
|
481
|
+
| ast.FunctionDef(name=name)
|
|
482
|
+
| ast.AsyncFunctionDef(name=name)
|
|
483
|
+
| ast.ClassDef(name=name)
|
|
484
|
+
| ast.ExceptHandler(name=str() as name)
|
|
485
|
+
| ast.MatchAs(name=str() as name)
|
|
486
|
+
| ast.MatchStar(name=str() as name)
|
|
487
|
+
| ast.MatchMapping(rest=str() as name)
|
|
488
|
+
):
|
|
480
489
|
return name == reference[0]
|
|
490
|
+
case ast.alias(name=imported, asname=alias):
|
|
491
|
+
return (alias or imported.partition(".")[0]) == reference[0]
|
|
481
492
|
case ast.Global(names=names) | ast.Nonlocal(names=names):
|
|
482
493
|
return reference[0] in names
|
|
483
494
|
case ast.Attribute(ctx=ast.Store() | ast.Del()):
|
|
@@ -51,6 +51,8 @@ class IacSourceCoupledTest(Rule):
|
|
|
51
51
|
"Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.",
|
|
52
52
|
"The Python detector currently owns Terraform and HCL suffixes; YAML remains with the general source-coupled rule.",
|
|
53
53
|
"Fixture, golden, and snapshot paths are treated as deliberate representation contracts; other packaging, formatter, and compatibility contracts require an exact suppression.",
|
|
54
|
+
"Lambda bodies and unconsumed generators are not inferred. Comprehension targets and match captures cannot inherit stale outer source provenance; proven builtin all/any line-content checks remain in scope.",
|
|
55
|
+
"Exact byte copies into proven temporary outputs and same-path byte preservation following a visible write_bytes of the compared named value remain unreported. Fixed-source byte equality and unproven or overwritten writes still require an exact representation-contract suppression.",
|
|
54
56
|
),
|
|
55
57
|
examples=(
|
|
56
58
|
RuleExample(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from collections import Counter
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
import os
|
|
6
7
|
from pathlib import Path, PurePosixPath
|
|
@@ -29,6 +30,14 @@ if TYPE_CHECKING:
|
|
|
29
30
|
|
|
30
31
|
_MIGRATION_PARTS = frozenset({"alembic", "migration", "migrations", "versions"})
|
|
31
32
|
_DESCRIPTOR_DECORATORS = frozenset({"classmethod", "staticmethod"})
|
|
33
|
+
_CLASS_PRESERVING_DECORATORS = frozenset(
|
|
34
|
+
{
|
|
35
|
+
("typing", "final"),
|
|
36
|
+
("typing", "runtime_checkable"),
|
|
37
|
+
("typing_extensions", "final"),
|
|
38
|
+
("typing_extensions", "runtime_checkable"),
|
|
39
|
+
}
|
|
40
|
+
)
|
|
32
41
|
_QUALIFIED_NAME_PARTS = 2
|
|
33
42
|
_SCAN_SKIP_PARTS = frozenset(
|
|
34
43
|
{
|
|
@@ -76,6 +85,7 @@ class _CanonicalSymbol(NamedTuple):
|
|
|
76
85
|
@dataclass(slots=True)
|
|
77
86
|
class RuntimeConfigFacts:
|
|
78
87
|
modules: dict[Path, ast.Module] = field(default_factory=dict)
|
|
88
|
+
stable_symbols: dict[Path, frozenset[str]] = field(default_factory=dict)
|
|
79
89
|
composition_calls: dict[tuple[Path, str, str], bool] = field(default_factory=dict)
|
|
80
90
|
canonical_symbols: dict[tuple[Path, str, str], _CanonicalSymbol] = field(default_factory=dict)
|
|
81
91
|
|
|
@@ -92,6 +102,7 @@ class NoHiddenConstructorFallback(Rule):
|
|
|
92
102
|
category=RuleCategory.ARCHITECTURE,
|
|
93
103
|
limitations=(
|
|
94
104
|
"Detection requires a proven local settings provider and a first-party composition call; covers None fallbacks and settings attributes captured directly in module-level class constructor defaults.",
|
|
105
|
+
"Provider instance, class, import and reexport names require one stable module binding. Reassignments, captures, global writers, wildcard imports and unknown class decorators conservatively exclude settings provenance; annotation-only declarations, read-only globals and stable typing final/runtime_checkable decorators remain valid.",
|
|
95
106
|
"Tests, generated files, migrations, descriptors, library environment fallbacks, and unconstructed classes are excluded.",
|
|
96
107
|
),
|
|
97
108
|
examples=(
|
|
@@ -412,6 +423,8 @@ class RuntimeConfigResolver:
|
|
|
412
423
|
return False
|
|
413
424
|
if parts[0] in shadowed:
|
|
414
425
|
return False
|
|
426
|
+
if parts[0] not in self._stable_symbols(_LoadedModule(self._tree, self._path)):
|
|
427
|
+
return False
|
|
415
428
|
resolved = _resolve_expression(expression, self._imports)
|
|
416
429
|
if resolved is None:
|
|
417
430
|
if self._module is None:
|
|
@@ -437,8 +450,12 @@ class RuntimeConfigResolver:
|
|
|
437
450
|
return False
|
|
438
451
|
tree = loaded.tree
|
|
439
452
|
module_path = loaded.path
|
|
453
|
+
stable = self._stable_symbols(loaded)
|
|
454
|
+
if symbol not in stable:
|
|
455
|
+
self._settings_cache[key] = False
|
|
456
|
+
return False
|
|
440
457
|
imports = _imports(tree, module, is_package=module_path.name == "__init__.py")
|
|
441
|
-
classes = _base_settings_classes(tree, imports)
|
|
458
|
+
classes = _base_settings_classes(tree, imports, stable)
|
|
442
459
|
factory = _assigned_factory(tree, symbol)
|
|
443
460
|
if factory is not None:
|
|
444
461
|
resolved_factory = _resolve_expression(factory, imports)
|
|
@@ -477,27 +494,47 @@ class RuntimeConfigResolver:
|
|
|
477
494
|
return False
|
|
478
495
|
tree = loaded.tree
|
|
479
496
|
module_path = loaded.path
|
|
497
|
+
stable = self._stable_symbols(loaded)
|
|
498
|
+
if symbol not in stable:
|
|
499
|
+
self._settings_class_cache[key] = False
|
|
500
|
+
return False
|
|
480
501
|
imports = _imports(tree, module, is_package=module_path.name == "__init__.py")
|
|
481
|
-
if symbol in _base_settings_classes(tree, imports):
|
|
502
|
+
if symbol in _base_settings_classes(tree, imports, stable):
|
|
482
503
|
self._settings_class_cache[key] = True
|
|
483
504
|
return True
|
|
484
505
|
class_node = next(
|
|
485
506
|
(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == symbol),
|
|
486
507
|
None,
|
|
487
508
|
)
|
|
488
|
-
result =
|
|
489
|
-
if class_node is not None:
|
|
490
|
-
for base in class_node.bases:
|
|
491
|
-
resolved = _resolve_expression(base, imports)
|
|
492
|
-
if resolved is None or len(resolved) < _QUALIFIED_NAME_PARTS:
|
|
493
|
-
continue
|
|
494
|
-
base_module, base_symbol = ".".join(resolved[:-1]), resolved[-1]
|
|
495
|
-
if self._is_settings_class(base_module, base_symbol, seen | {key}):
|
|
496
|
-
result = True
|
|
497
|
-
break
|
|
509
|
+
result = class_node is not None and self._inherits_settings_class(class_node, imports, stable, seen | {key})
|
|
498
510
|
self._settings_class_cache[key] = result
|
|
499
511
|
return result
|
|
500
512
|
|
|
513
|
+
def _inherits_settings_class(
|
|
514
|
+
self,
|
|
515
|
+
node: ast.ClassDef,
|
|
516
|
+
imports: dict[str, _Binding],
|
|
517
|
+
stable: frozenset[str],
|
|
518
|
+
seen: frozenset[tuple[str, str]],
|
|
519
|
+
) -> bool:
|
|
520
|
+
if not _class_decorators_preserve_type(node, imports, stable):
|
|
521
|
+
return False
|
|
522
|
+
for base in node.bases:
|
|
523
|
+
resolved = _stable_import_reference(base, imports, stable)
|
|
524
|
+
if resolved is None or len(resolved) < _QUALIFIED_NAME_PARTS:
|
|
525
|
+
continue
|
|
526
|
+
base_module, base_symbol = ".".join(resolved[:-1]), resolved[-1]
|
|
527
|
+
if self._is_settings_class(base_module, base_symbol, seen):
|
|
528
|
+
return True
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
def _stable_symbols(self, loaded: _LoadedModule) -> frozenset[str]:
|
|
532
|
+
cached = self._facts.stable_symbols.get(loaded.path)
|
|
533
|
+
if cached is None:
|
|
534
|
+
cached = _stable_module_symbols(loaded.tree)
|
|
535
|
+
self._facts.stable_symbols[loaded.path] = cached
|
|
536
|
+
return cached
|
|
537
|
+
|
|
501
538
|
def _load_module(self, module: str) -> _LoadedModule | None:
|
|
502
539
|
if module == self._module:
|
|
503
540
|
return _LoadedModule(self._tree, self._path)
|
|
@@ -558,12 +595,21 @@ def _attribute_parts(expression: ast.expr) -> tuple[str, ...] | None:
|
|
|
558
595
|
return None
|
|
559
596
|
|
|
560
597
|
|
|
561
|
-
def _base_settings_classes(tree: ast.Module, imports: dict[str, _Binding]) -> set[str]:
|
|
562
|
-
classes = [
|
|
598
|
+
def _base_settings_classes(tree: ast.Module, imports: dict[str, _Binding], stable: frozenset[str]) -> set[str]:
|
|
599
|
+
classes = [
|
|
600
|
+
statement
|
|
601
|
+
for statement in tree.body
|
|
602
|
+
if isinstance(statement, ast.ClassDef)
|
|
603
|
+
and statement.name in stable
|
|
604
|
+
and _class_decorators_preserve_type(statement, imports, stable)
|
|
605
|
+
]
|
|
563
606
|
found = {
|
|
564
607
|
node.name
|
|
565
608
|
for node in classes
|
|
566
|
-
if any(
|
|
609
|
+
if any(
|
|
610
|
+
_stable_import_reference(base, imports, stable) == ("pydantic_settings", "BaseSettings")
|
|
611
|
+
for base in node.bases
|
|
612
|
+
)
|
|
567
613
|
}
|
|
568
614
|
changed = True
|
|
569
615
|
while changed:
|
|
@@ -575,6 +621,44 @@ def _base_settings_classes(tree: ast.Module, imports: dict[str, _Binding]) -> se
|
|
|
575
621
|
return found
|
|
576
622
|
|
|
577
623
|
|
|
624
|
+
def _class_decorators_preserve_type(node: ast.ClassDef, imports: dict[str, _Binding], stable: frozenset[str]) -> bool:
|
|
625
|
+
return all(
|
|
626
|
+
_stable_import_reference(decorator, imports, stable) in _CLASS_PRESERVING_DECORATORS
|
|
627
|
+
for decorator in node.decorator_list
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _stable_import_reference(
|
|
632
|
+
expression: ast.expr, imports: dict[str, _Binding], stable: frozenset[str]
|
|
633
|
+
) -> tuple[str, ...] | None:
|
|
634
|
+
parts = _attribute_parts(expression)
|
|
635
|
+
if parts is None or parts[0] not in stable:
|
|
636
|
+
return None
|
|
637
|
+
return _resolve_expression(expression, imports)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _stable_module_symbols(tree: ast.Module) -> frozenset[str]:
|
|
641
|
+
if any(
|
|
642
|
+
isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names) for node in ast.walk(tree)
|
|
643
|
+
):
|
|
644
|
+
return frozenset()
|
|
645
|
+
bindings: Counter[str] = Counter()
|
|
646
|
+
for statement in tree.body:
|
|
647
|
+
if isinstance(statement, ast.AnnAssign) and statement.value is None:
|
|
648
|
+
continue
|
|
649
|
+
collector = LocalBindingCollector()
|
|
650
|
+
collector.visit(statement)
|
|
651
|
+
bindings.update(collector.names)
|
|
652
|
+
global_writes: set[str] = set()
|
|
653
|
+
for node in ast.walk(tree):
|
|
654
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
655
|
+
collector = LocalBindingCollector()
|
|
656
|
+
for statement in node.body:
|
|
657
|
+
collector.visit(statement)
|
|
658
|
+
global_writes.update(collector.names & collector.globals)
|
|
659
|
+
return frozenset(name for name, count in bindings.items() if count == 1 and name not in global_writes)
|
|
660
|
+
|
|
661
|
+
|
|
578
662
|
def _assigned_factory(tree: ast.Module, symbol: str) -> ast.expr | None:
|
|
579
663
|
for statement in tree.body:
|
|
580
664
|
if isinstance(statement, ast.Assign):
|
|
@@ -842,9 +926,32 @@ class LocalBindingCollector(ast.NodeVisitor):
|
|
|
842
926
|
|
|
843
927
|
@override
|
|
844
928
|
def visit_Name(self, node: ast.Name) -> None:
|
|
845
|
-
if isinstance(node.ctx, ast.Store):
|
|
929
|
+
if isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
846
930
|
self.names.add(node.id)
|
|
847
931
|
|
|
932
|
+
@override
|
|
933
|
+
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
934
|
+
if node.name is not None:
|
|
935
|
+
self.names.add(node.name)
|
|
936
|
+
self.generic_visit(node)
|
|
937
|
+
|
|
938
|
+
@override
|
|
939
|
+
def visit_MatchAs(self, node: ast.MatchAs) -> None:
|
|
940
|
+
if node.name is not None:
|
|
941
|
+
self.names.add(node.name)
|
|
942
|
+
self.generic_visit(node)
|
|
943
|
+
|
|
944
|
+
@override
|
|
945
|
+
def visit_MatchStar(self, node: ast.MatchStar) -> None:
|
|
946
|
+
if node.name is not None:
|
|
947
|
+
self.names.add(node.name)
|
|
948
|
+
|
|
949
|
+
@override
|
|
950
|
+
def visit_MatchMapping(self, node: ast.MatchMapping) -> None:
|
|
951
|
+
if node.rest is not None:
|
|
952
|
+
self.names.add(node.rest)
|
|
953
|
+
self.generic_visit(node)
|
|
954
|
+
|
|
848
955
|
@override
|
|
849
956
|
def visit_Import(self, node: ast.Import) -> None:
|
|
850
957
|
self.names.update(alias.asname or alias.name.partition(".")[0] for alias in node.names)
|
|
@@ -860,34 +967,60 @@ class LocalBindingCollector(ast.NodeVisitor):
|
|
|
860
967
|
@override
|
|
861
968
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
862
969
|
self.names.add(node.name)
|
|
970
|
+
for outer in (
|
|
971
|
+
*node.decorator_list,
|
|
972
|
+
*node.args.defaults,
|
|
973
|
+
*(default for default in node.args.kw_defaults if default is not None),
|
|
974
|
+
):
|
|
975
|
+
self.visit(outer)
|
|
863
976
|
|
|
864
977
|
@override
|
|
865
978
|
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
866
979
|
self.names.add(node.name)
|
|
980
|
+
for outer in (
|
|
981
|
+
*node.decorator_list,
|
|
982
|
+
*node.args.defaults,
|
|
983
|
+
*(default for default in node.args.kw_defaults if default is not None),
|
|
984
|
+
):
|
|
985
|
+
self.visit(outer)
|
|
867
986
|
|
|
868
987
|
@override
|
|
869
988
|
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
870
989
|
self.names.add(node.name)
|
|
990
|
+
for outer in (*node.decorator_list, *node.bases, *node.keywords):
|
|
991
|
+
self.visit(outer)
|
|
871
992
|
|
|
872
993
|
@override
|
|
873
994
|
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
874
|
-
|
|
995
|
+
for default in (*node.args.defaults, *(value for value in node.args.kw_defaults if value is not None)):
|
|
996
|
+
self.visit(default)
|
|
875
997
|
|
|
876
998
|
@override
|
|
877
999
|
def visit_ListComp(self, node: ast.ListComp) -> None:
|
|
878
|
-
|
|
1000
|
+
self.visit(node.elt)
|
|
1001
|
+
self._visit_generators(node.generators)
|
|
879
1002
|
|
|
880
1003
|
@override
|
|
881
1004
|
def visit_SetComp(self, node: ast.SetComp) -> None:
|
|
882
|
-
|
|
1005
|
+
self.visit(node.elt)
|
|
1006
|
+
self._visit_generators(node.generators)
|
|
883
1007
|
|
|
884
1008
|
@override
|
|
885
1009
|
def visit_DictComp(self, node: ast.DictComp) -> None:
|
|
886
|
-
|
|
1010
|
+
self.visit(node.key)
|
|
1011
|
+
self.visit(node.value)
|
|
1012
|
+
self._visit_generators(node.generators)
|
|
887
1013
|
|
|
888
1014
|
@override
|
|
889
1015
|
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
|
|
890
|
-
|
|
1016
|
+
self.visit(node.elt)
|
|
1017
|
+
self._visit_generators(node.generators)
|
|
1018
|
+
|
|
1019
|
+
def _visit_generators(self, generators: list[ast.comprehension]) -> None:
|
|
1020
|
+
for generator in generators:
|
|
1021
|
+
self.visit(generator.iter)
|
|
1022
|
+
for condition in generator.ifs:
|
|
1023
|
+
self.visit(condition)
|
|
891
1024
|
|
|
892
1025
|
|
|
893
1026
|
def _bind_import_modules(statement: ast.Import, bindings: dict[str, _Binding]) -> None:
|
|
@@ -53,7 +53,7 @@ class NoInjectedModuleLoader(Rule):
|
|
|
53
53
|
limitations=(
|
|
54
54
|
"Flags defaults resolving to importlib.import_module, importlib.__import__, or builtins.__import__, including stable module-level import and single-assignment aliases and unshadowed __import__.",
|
|
55
55
|
"Tests, test support, and generated code are excluded. Required loader parameters, application loaders, wrapper lambdas, alias chains, comprehensions, and imports local to enclosing scopes are not inferred.",
|
|
56
|
-
"Conservatively skips shadowed or rebound names, wildcard-import files, reassigned loader attributes, and exception or pattern captures of importer roots, symbols or assignment aliases. A relevant-name capture in an unrelated local scope can cause a false negative because capture checks are file-wide.",
|
|
56
|
+
"Conservatively skips shadowed or rebound names, wildcard-import files, reassigned loader attributes, conflicting conditional or relative imports, and exception or pattern captures of importer roots, symbols or assignment aliases. Identical repeated importer imports remain valid. A relevant-name capture in an unrelated local scope can cause a false negative because capture checks are file-wide.",
|
|
57
57
|
"Assignment aliases require one module binding and precede use by line and column. Defaults use their enclosing evaluation scope: earlier statements in the nearest executing class body can shadow loaders; methods and nested classes do not close over class attributes. Enclosing function body bindings remain conservative exclusions, but a function's parameters do not shadow its own defaults. Relevant global declarations and walrus writes exclude sensitive importer names. Compound class statements can cause conservative omissions. Dynamic namespace mutation is not inferred. No autofix: changing arguments or import placement can alter API and initialization behavior.",
|
|
58
58
|
),
|
|
59
59
|
examples=(
|
|
@@ -134,6 +134,8 @@ def _ambiguous_loader_bindings(context: PythonFileContext, aliases: Mapping[str,
|
|
|
134
134
|
if (target.module in {"importlib", "builtins"} and target.symbol is None)
|
|
135
135
|
or f"{target.module}.{target.symbol}" in _LOADER_TARGETS
|
|
136
136
|
}
|
|
137
|
+
if _conflicting_loader_imports(context, sensitive_names):
|
|
138
|
+
return True
|
|
137
139
|
if any(sensitive_names.intersection(node.names) for node in context.nodes(ast.Global)):
|
|
138
140
|
return True
|
|
139
141
|
for node in context.nodes(ast.ExceptHandler, ast.MatchAs, ast.MatchStar, ast.MatchMapping, ast.NamedExpr):
|
|
@@ -154,6 +156,35 @@ def _ambiguous_loader_bindings(context: PythonFileContext, aliases: Mapping[str,
|
|
|
154
156
|
)
|
|
155
157
|
|
|
156
158
|
|
|
159
|
+
def _conflicting_loader_imports(context: PythonFileContext, sensitive_names: set[str]) -> bool:
|
|
160
|
+
for statement in context.nodes(ast.Import, ast.ImportFrom):
|
|
161
|
+
if not _is_module_binding(statement, context):
|
|
162
|
+
continue
|
|
163
|
+
for alias in statement.names:
|
|
164
|
+
name = alias.asname or alias.name.partition(".")[0]
|
|
165
|
+
if name in sensitive_names and _loader_import_conflicts(statement, alias, name, context):
|
|
166
|
+
return True
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _loader_import_conflicts(
|
|
171
|
+
statement: ast.Import | ast.ImportFrom,
|
|
172
|
+
alias: ast.alias,
|
|
173
|
+
name: str,
|
|
174
|
+
context: PythonFileContext,
|
|
175
|
+
) -> bool:
|
|
176
|
+
if isinstance(statement, ast.ImportFrom):
|
|
177
|
+
if statement.level or statement.module is None:
|
|
178
|
+
return True
|
|
179
|
+
imported = (statement.module, alias.name)
|
|
180
|
+
else:
|
|
181
|
+
imported = (alias.name if alias.asname else alias.name.partition(".")[0], None)
|
|
182
|
+
binding = context.module_imports.bindings.get(name)
|
|
183
|
+
if binding is not None:
|
|
184
|
+
return imported != (binding.module, binding.symbol)
|
|
185
|
+
return name == "__import__" and ".".join(part for part in imported if part is not None) not in _LOADER_TARGETS
|
|
186
|
+
|
|
187
|
+
|
|
157
188
|
def _defaults(arguments: ast.arguments) -> Iterator[tuple[ast.arg, ast.expr]]:
|
|
158
189
|
positional = (*arguments.posonlyargs, *arguments.args)
|
|
159
190
|
defaulted = positional[-len(arguments.defaults) :] if arguments.defaults else ()
|
|
@@ -67,7 +67,7 @@ class NoNullableDependencyFallback(Rule):
|
|
|
67
67
|
limitations=(
|
|
68
68
|
"Warns for None-defaulted function parameters with a fallback used as a callable or a proven application-settings fallback; genuine absent state is allowed.",
|
|
69
69
|
"Constructors remain owned by SARJ095 and SARJ468. Decorated functions, tests, generated code, nested scope captures, non-callable object fallbacks, and interprocedural forwarding are excluded.",
|
|
70
|
-
"Callable use through a local assignment, annotation or None guard requires at most one body binding for that name and a call after the binding statement completes. Rebinding, deletion, imports, definitions, pattern or exception captures, global/nonlocal declarations and nested-scope bindings conservatively exclude that inference; direct inline calls and
|
|
70
|
+
"Callable use through a local assignment, annotation or None guard requires at most one body binding for that name and a call after the binding statement completes. Rebinding, deletion, imports, definitions, pattern or exception captures, global/nonlocal declarations and nested-scope bindings conservatively exclude that inference; direct inline calls remain independent. Settings provenance also excludes enclosing bindings and names written by a function declaring them global, while read-only global declarations remain valid.",
|
|
71
71
|
"No autofix: removing explicit None acceptance changes the callable contract, and moving a fallback can change initialization timing or error handling.",
|
|
72
72
|
),
|
|
73
73
|
examples=(
|
|
@@ -123,7 +123,7 @@ class NoNullableDependencyFallback(Rule):
|
|
|
123
123
|
parameters = _none_default_parameters(function.args)
|
|
124
124
|
if not parameters:
|
|
125
125
|
continue
|
|
126
|
-
hidden = _hidden_dependencies(function, parameters, resolver)
|
|
126
|
+
hidden = _hidden_dependencies(function, parameters, resolver, _settings_shadowed(function, context))
|
|
127
127
|
findings.extend(
|
|
128
128
|
Diagnostic(
|
|
129
129
|
path=context.path,
|
|
@@ -156,11 +156,11 @@ def _hidden_dependencies(
|
|
|
156
156
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
157
157
|
parameters: dict[str, ast.arg],
|
|
158
158
|
resolver: RuntimeConfigResolver,
|
|
159
|
+
shadowed: set[str],
|
|
159
160
|
) -> set[str]:
|
|
160
161
|
usage = _scope_usage(function)
|
|
161
162
|
candidates = set(parameters)
|
|
162
163
|
hidden: set[str] = set()
|
|
163
|
-
shadowed = set(scope_bindings(function))
|
|
164
164
|
for statement in function.body:
|
|
165
165
|
guard = _guarded_fallback(statement, candidates)
|
|
166
166
|
bindings = LocalBindingCollector()
|
|
@@ -187,6 +187,19 @@ def _hidden_dependencies(
|
|
|
187
187
|
return hidden
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def _settings_shadowed(
|
|
191
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
192
|
+
context: PythonFileContext,
|
|
193
|
+
) -> set[str]:
|
|
194
|
+
shadowed: set[str] = set()
|
|
195
|
+
current: ast.AST | None = function
|
|
196
|
+
while current is not None:
|
|
197
|
+
if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
|
|
198
|
+
shadowed.update(scope_bindings(current))
|
|
199
|
+
current = context.parents.get(current)
|
|
200
|
+
return shadowed
|
|
201
|
+
|
|
202
|
+
|
|
190
203
|
def _scope_usage(function: ast.FunctionDef | ast.AsyncFunctionDef) -> _ScopeUsage:
|
|
191
204
|
nodes = [node for statement in function.body for node in _scope_nodes(statement)]
|
|
192
205
|
parents = {child: node for node in nodes for child in ast.iter_child_nodes(node)}
|