sarj-python-lint 1.29.0__tar.gz → 1.30.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/pyproject.toml +1 -1
- sarj_python_lint-1.30.0/src/sarj_python_lint/rules/_exports.py +27 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_namespace_access.py +3 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_pytest.py +16 -0
- sarj_python_lint-1.30.0/src/sarj_python_lint/rules/_resource_provenance.py +357 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +5 -3
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +2 -12
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +305 -5
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +2 -2
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +2 -24
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +2 -12
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +2 -23
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +2 -2
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +2 -2
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +52 -5
- sarj_python_lint-1.29.0/src/sarj_python_lint/rules/_resource_provenance.py +0 -200
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/.gitignore +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/LICENSE +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/README.md +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.29.0 → sarj_python_lint-1.30.0}/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.
|
|
3
|
+
Version: 1.30.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def mentions_dunder_all(node: ast.AST) -> bool:
|
|
9
|
+
for child in walk(node):
|
|
10
|
+
match child:
|
|
11
|
+
case ast.Name(id="__all__"):
|
|
12
|
+
return True
|
|
13
|
+
case ast.alias() if (child.asname or child.name).split(".")[0] == "__all__":
|
|
14
|
+
return True
|
|
15
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef() if child.name == "__all__":
|
|
16
|
+
return True
|
|
17
|
+
case ast.arg(arg="__all__") | ast.ExceptHandler(name="__all__"):
|
|
18
|
+
return True
|
|
19
|
+
case ast.MatchAs() | ast.MatchStar() if child.name == "__all__":
|
|
20
|
+
return True
|
|
21
|
+
case ast.MatchMapping(rest="__all__"):
|
|
22
|
+
return True
|
|
23
|
+
case ast.Global() | ast.Nonlocal() if "__all__" in child.names:
|
|
24
|
+
return True
|
|
25
|
+
case _:
|
|
26
|
+
pass
|
|
27
|
+
return False
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from sarj_python_lint.rules._ast_index import walk
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def collected_tests(statements: list[ast.stmt]) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
14
|
+
for statement in statements:
|
|
15
|
+
match statement:
|
|
16
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
|
|
17
|
+
yield statement
|
|
18
|
+
case ast.ClassDef() if statement.name.startswith("Test"):
|
|
19
|
+
yield from collected_tests(statement.body)
|
|
20
|
+
case _:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
8
24
|
# The pytest-benchmark fixture: the test measures time, it does not verify.
|
|
9
25
|
_BENCHMARK = "benchmark"
|
|
10
26
|
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import TYPE_CHECKING, final
|
|
8
|
+
|
|
9
|
+
from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_SCOPES = (
|
|
17
|
+
ast.Module,
|
|
18
|
+
ast.FunctionDef,
|
|
19
|
+
ast.AsyncFunctionDef,
|
|
20
|
+
ast.ClassDef,
|
|
21
|
+
ast.Lambda,
|
|
22
|
+
ast.ListComp,
|
|
23
|
+
ast.SetComp,
|
|
24
|
+
ast.DictComp,
|
|
25
|
+
ast.GeneratorExp,
|
|
26
|
+
)
|
|
27
|
+
_RESOURCE_OWNERS = MappingProxyType(
|
|
28
|
+
{
|
|
29
|
+
"contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
|
|
30
|
+
"subprocess": frozenset({"Popen", "TimeoutExpired"}),
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class ResourceProvenance:
|
|
37
|
+
def __init__(self, context: PythonFileContext, *, require_static_namespace: bool = False) -> None:
|
|
38
|
+
self.context = context
|
|
39
|
+
self.require_static_namespace = require_static_namespace
|
|
40
|
+
self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
|
|
41
|
+
self.reads: dict[str, list[ast.Name]] = defaultdict(list)
|
|
42
|
+
self._reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
|
|
43
|
+
self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
|
|
44
|
+
self.async_contract_cache: dict[ast.AsyncFunctionDef, bool] = {}
|
|
45
|
+
self.import_contract_cache: dict[str, bool] = {}
|
|
46
|
+
self.wildcard = False
|
|
47
|
+
self.mutated: set[str] = set()
|
|
48
|
+
self.external_bindings: set[str] = set()
|
|
49
|
+
for node in context.nodes(ast.AST):
|
|
50
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
|
51
|
+
self.reads[node.id].append(node)
|
|
52
|
+
|
|
53
|
+
for name in _bound_names(node):
|
|
54
|
+
self.bindings[self.scope(node)][name].append(node)
|
|
55
|
+
self.wildcard |= name == "*"
|
|
56
|
+
if isinstance(node, ast.Global | ast.Nonlocal):
|
|
57
|
+
self.external_bindings.update(node.names)
|
|
58
|
+
if isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store | ast.Del):
|
|
59
|
+
root = _root_name(node)
|
|
60
|
+
if root is not None:
|
|
61
|
+
self.mutated.add(root)
|
|
62
|
+
|
|
63
|
+
@cached_property
|
|
64
|
+
def builtin_namespace_untouched(self) -> bool:
|
|
65
|
+
return not any(
|
|
66
|
+
(isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
|
|
67
|
+
or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
|
|
68
|
+
or (isinstance(node, ast.Name) and node.id == "__builtins__")
|
|
69
|
+
for node in self.context.nodes(ast.Import, ast.ImportFrom, ast.Name)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def scope(self, node: ast.AST) -> ast.AST | None:
|
|
73
|
+
parent = self.context.parents.get(node)
|
|
74
|
+
while parent is not None and not isinstance(parent, _SCOPES):
|
|
75
|
+
parent = self.context.parents.get(parent)
|
|
76
|
+
return parent
|
|
77
|
+
|
|
78
|
+
def binding(self, name: str, at: ast.AST) -> ast.AST | None:
|
|
79
|
+
scope = self.scope(at)
|
|
80
|
+
while scope is not None:
|
|
81
|
+
nodes = self.bindings.get(scope, {}).get(name)
|
|
82
|
+
if nodes:
|
|
83
|
+
return nodes[0] if len(nodes) == 1 else None
|
|
84
|
+
scope = self.scope(scope)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def imported(self, node: ast.expr, qualified: str) -> bool:
|
|
88
|
+
root = _root_name(node)
|
|
89
|
+
if (
|
|
90
|
+
self.wildcard
|
|
91
|
+
or self.dynamic_namespace
|
|
92
|
+
or root is None
|
|
93
|
+
or root in self.mutated
|
|
94
|
+
or not self._owner_namespace_untouched(qualified.partition(".")[0])
|
|
95
|
+
):
|
|
96
|
+
return False
|
|
97
|
+
binding = self.binding(root, node)
|
|
98
|
+
for read in self.reads.get(root, ()):
|
|
99
|
+
parent = self.context.parents.get(read)
|
|
100
|
+
if isinstance(parent, ast.Attribute | ast.ExceptHandler):
|
|
101
|
+
continue
|
|
102
|
+
if not (isinstance(parent, ast.Call) and parent.func is read):
|
|
103
|
+
return False
|
|
104
|
+
return (
|
|
105
|
+
isinstance(binding, ast.alias)
|
|
106
|
+
and isinstance(self.scope(binding), ast.Module)
|
|
107
|
+
and self.context.imports.resolved_qualified_name(node) == qualified
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _owner_namespace_untouched(self, module: str) -> bool:
|
|
111
|
+
if module not in _RESOURCE_OWNERS:
|
|
112
|
+
return True
|
|
113
|
+
if module not in self.import_contract_cache:
|
|
114
|
+
self.import_contract_cache[module] = self._inspect_owner_namespace(module)
|
|
115
|
+
return self.import_contract_cache[module]
|
|
116
|
+
|
|
117
|
+
def _inspect_owner_namespace(self, module: str) -> bool:
|
|
118
|
+
origins: dict[str, set[str]] = defaultdict(set)
|
|
119
|
+
for node in self.context.nodes(ast.Import, ast.ImportFrom):
|
|
120
|
+
for name, target in _owner_import_targets(node, module).items():
|
|
121
|
+
origins[name].add(target)
|
|
122
|
+
if not self.mutated.isdisjoint(origins):
|
|
123
|
+
return False
|
|
124
|
+
owners = frozenset(f"{module}.{name}" for name in _RESOURCE_OWNERS[module])
|
|
125
|
+
return all(
|
|
126
|
+
self._safe_import_use(node)
|
|
127
|
+
for node in self.context.nodes(ast.Name, ast.Attribute)
|
|
128
|
+
if self._owner_reference(node, origins, owners)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _owner_reference(
|
|
132
|
+
self, node: ast.Name | ast.Attribute, origins: dict[str, set[str]], owners: frozenset[str]
|
|
133
|
+
) -> bool:
|
|
134
|
+
if not isinstance(node.ctx, ast.Load):
|
|
135
|
+
return False
|
|
136
|
+
if isinstance(node, ast.Name):
|
|
137
|
+
return node.id in origins
|
|
138
|
+
return any(
|
|
139
|
+
qualified == owner or qualified.startswith(owner + ".")
|
|
140
|
+
for qualified in _owner_attribute_targets(node, origins)
|
|
141
|
+
for owner in owners
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _safe_import_use(self, node: ast.Name | ast.Attribute) -> bool:
|
|
145
|
+
parent = self.context.parents.get(node)
|
|
146
|
+
return (
|
|
147
|
+
(isinstance(parent, ast.Attribute) and parent.value is node)
|
|
148
|
+
or (isinstance(parent, ast.Call) and parent.func is node)
|
|
149
|
+
or (isinstance(parent, ast.ExceptHandler) and parent.type is node)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def constructor(self, name: ast.Name, at: ast.AST) -> ast.Call | None:
|
|
153
|
+
binding = self.binding(name.id, at)
|
|
154
|
+
if not isinstance(binding, ast.Name):
|
|
155
|
+
return None
|
|
156
|
+
statement = self.context.parents.get(binding)
|
|
157
|
+
if isinstance(statement, ast.withitem):
|
|
158
|
+
owner = self.context.parents.get(statement)
|
|
159
|
+
if (
|
|
160
|
+
binding is statement.optional_vars
|
|
161
|
+
and self.contains(owner, at)
|
|
162
|
+
and isinstance(statement.context_expr, ast.Call)
|
|
163
|
+
):
|
|
164
|
+
return statement.context_expr
|
|
165
|
+
return None
|
|
166
|
+
value = self.assignment_value(binding, at)
|
|
167
|
+
return value if isinstance(value, ast.Call) else None
|
|
168
|
+
|
|
169
|
+
def assignment_value(self, binding: ast.Name, at: ast.AST) -> ast.expr | None:
|
|
170
|
+
statement = self.context.parents.get(binding)
|
|
171
|
+
if not isinstance(statement, ast.Assign | ast.AnnAssign):
|
|
172
|
+
return None
|
|
173
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) != 1:
|
|
174
|
+
return None
|
|
175
|
+
if not isinstance(self.context.parents.get(statement), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
176
|
+
return None
|
|
177
|
+
return statement.value if statement.lineno < getattr(at, "lineno", 0) else None
|
|
178
|
+
|
|
179
|
+
def async_function(self, value: ast.expr, at: ast.AST) -> ast.AsyncFunctionDef | None:
|
|
180
|
+
if self.dynamic_namespace:
|
|
181
|
+
return None
|
|
182
|
+
seen: set[str] = set()
|
|
183
|
+
while isinstance(value, ast.Name) and value.id not in seen:
|
|
184
|
+
seen.add(value.id)
|
|
185
|
+
if value.id in self.mutated:
|
|
186
|
+
return None
|
|
187
|
+
binding = self.binding(value.id, at)
|
|
188
|
+
if isinstance(binding, ast.AsyncFunctionDef):
|
|
189
|
+
return self._stable_async_definition(binding)
|
|
190
|
+
if not isinstance(binding, ast.Name):
|
|
191
|
+
return None
|
|
192
|
+
resolved = self.assignment_value(binding, at)
|
|
193
|
+
if resolved is None:
|
|
194
|
+
return None
|
|
195
|
+
value, at = resolved, binding
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
@cached_property
|
|
199
|
+
def dynamic_namespace(self) -> bool:
|
|
200
|
+
return self.require_static_namespace and dynamic_namespace_access(
|
|
201
|
+
self.context.nodes(ast.AST), self.context.imports, self.context.parents
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _stable_async_definition(self, function: ast.AsyncFunctionDef) -> ast.AsyncFunctionDef | None:
|
|
205
|
+
owner = self.context.parents.get(function)
|
|
206
|
+
if not isinstance(owner, ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
207
|
+
return None
|
|
208
|
+
if function.decorator_list or any(isinstance(node, ast.Yield | ast.YieldFrom) for node in ast.walk(function)):
|
|
209
|
+
return None
|
|
210
|
+
if function not in self.async_contract_cache:
|
|
211
|
+
self.async_contract_cache[function] = self._async_aliases_untouched(function)
|
|
212
|
+
return function if self.async_contract_cache[function] else None
|
|
213
|
+
|
|
214
|
+
def _async_aliases_untouched(self, function: ast.AsyncFunctionDef) -> bool:
|
|
215
|
+
pending: list[ast.AsyncFunctionDef | ast.Name] = [function]
|
|
216
|
+
seen: set[ast.AST] = set()
|
|
217
|
+
while pending:
|
|
218
|
+
binding = pending.pop()
|
|
219
|
+
if binding in seen:
|
|
220
|
+
continue
|
|
221
|
+
seen.add(binding)
|
|
222
|
+
name = binding.id if isinstance(binding, ast.Name) else binding.name
|
|
223
|
+
if name in self.mutated or name in self.external_bindings:
|
|
224
|
+
return False
|
|
225
|
+
for read in self._binding_reads(name, binding):
|
|
226
|
+
alias = self._function_alias(read)
|
|
227
|
+
if alias is not None:
|
|
228
|
+
pending.append(alias)
|
|
229
|
+
elif not self._safe_async_read(read):
|
|
230
|
+
return False
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
def _function_alias(self, read: ast.Name) -> ast.Name | None:
|
|
234
|
+
parent = self.context.parents.get(read)
|
|
235
|
+
if not isinstance(parent, ast.Assign | ast.AnnAssign) or parent.value is not read:
|
|
236
|
+
return None
|
|
237
|
+
if not isinstance(self.context.parents.get(parent), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
238
|
+
return None
|
|
239
|
+
targets = parent.targets if isinstance(parent, ast.Assign) else [parent.target]
|
|
240
|
+
if len(targets) != 1 or not isinstance(targets[0], ast.Name):
|
|
241
|
+
return None
|
|
242
|
+
target = targets[0]
|
|
243
|
+
return target if self.binding(target.id, target) is target else None
|
|
244
|
+
|
|
245
|
+
def _safe_async_read(self, read: ast.Name) -> bool:
|
|
246
|
+
parent = self.context.parents.get(read)
|
|
247
|
+
if not isinstance(parent, ast.Call):
|
|
248
|
+
return False
|
|
249
|
+
if parent.func is read:
|
|
250
|
+
return True
|
|
251
|
+
if not parent.args or parent.args[0] is not read or not isinstance(parent.func, ast.Attribute):
|
|
252
|
+
return False
|
|
253
|
+
receiver = parent.func.value
|
|
254
|
+
if parent.func.attr not in {"callback", "push_async_callback"} or not isinstance(receiver, ast.Name):
|
|
255
|
+
return False
|
|
256
|
+
if not self.unescaped(receiver, parent, allow_context=True):
|
|
257
|
+
return False
|
|
258
|
+
constructor = self.constructor(receiver, parent)
|
|
259
|
+
return (
|
|
260
|
+
constructor is not None
|
|
261
|
+
and not constructor.args
|
|
262
|
+
and not constructor.keywords
|
|
263
|
+
and any(self.imported(constructor.func, f"contextlib.{kind}") for kind in ("ExitStack", "AsyncExitStack"))
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _binding_reads(self, name: str, binding: ast.AST) -> list[ast.Name]:
|
|
267
|
+
if name not in self._reads_by_binding:
|
|
268
|
+
grouped: dict[ast.AST, list[ast.Name]] = defaultdict(list)
|
|
269
|
+
for read in self.reads.get(name, ()):
|
|
270
|
+
if (owner := self.binding(read.id, read)) is not None:
|
|
271
|
+
grouped[owner].append(read)
|
|
272
|
+
self._reads_by_binding[name] = grouped
|
|
273
|
+
return self._reads_by_binding[name].get(binding, [])
|
|
274
|
+
|
|
275
|
+
def unescaped(self, name: ast.Name, at: ast.AST, *, allow_context: bool = False) -> bool:
|
|
276
|
+
binding = self.binding(name.id, at)
|
|
277
|
+
if self.dynamic_namespace or binding is None or name.id in self.mutated:
|
|
278
|
+
return False
|
|
279
|
+
key = (binding, allow_context)
|
|
280
|
+
if key not in self.escape_cache:
|
|
281
|
+
self.escape_cache[key] = all(
|
|
282
|
+
self._method_or_context_read(read, allow_context=allow_context)
|
|
283
|
+
for read in self._binding_reads(name.id, binding)
|
|
284
|
+
)
|
|
285
|
+
return self.escape_cache[key]
|
|
286
|
+
|
|
287
|
+
def _method_or_context_read(self, read: ast.Name, *, allow_context: bool) -> bool:
|
|
288
|
+
parent = self.context.parents.get(read)
|
|
289
|
+
if allow_context and isinstance(parent, ast.withitem) and parent.context_expr is read:
|
|
290
|
+
return True
|
|
291
|
+
if (
|
|
292
|
+
not isinstance(parent, ast.Attribute)
|
|
293
|
+
or parent.value is not read
|
|
294
|
+
or parent.attr in {"__setattr__", "__delattr__", "__getattribute__"}
|
|
295
|
+
):
|
|
296
|
+
return False
|
|
297
|
+
call = self.context.parents.get(parent)
|
|
298
|
+
return isinstance(call, ast.Call) and call.func is parent
|
|
299
|
+
|
|
300
|
+
def contains(self, owner: ast.AST | None, node: ast.AST) -> bool:
|
|
301
|
+
while node in self.context.parents:
|
|
302
|
+
node = self.context.parents[node]
|
|
303
|
+
if node is owner:
|
|
304
|
+
return True
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _owner_import_targets(node: ast.Import | ast.ImportFrom, module: str) -> dict[str, str]:
|
|
309
|
+
if isinstance(node, ast.Import):
|
|
310
|
+
return {alias.asname or alias.name: module for alias in node.names if alias.name == module}
|
|
311
|
+
if node.module == module:
|
|
312
|
+
return {
|
|
313
|
+
alias.asname or alias.name: f"{module}.{alias.name}"
|
|
314
|
+
for alias in node.names
|
|
315
|
+
if alias.name in _RESOURCE_OWNERS[module]
|
|
316
|
+
}
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _owner_attribute_targets(node: ast.Attribute, origins: dict[str, set[str]]) -> frozenset[str]:
|
|
321
|
+
parts: list[str] = []
|
|
322
|
+
current: ast.expr = node
|
|
323
|
+
while isinstance(current, ast.Attribute):
|
|
324
|
+
parts.append(current.attr)
|
|
325
|
+
current = current.value
|
|
326
|
+
if not isinstance(current, ast.Name):
|
|
327
|
+
return frozenset()
|
|
328
|
+
suffix = ".".join(reversed(parts))
|
|
329
|
+
return frozenset(f"{origin}.{suffix}" for origin in origins.get(current.id, ()))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _root_name(node: ast.expr) -> str | None:
|
|
333
|
+
while isinstance(node, ast.Attribute):
|
|
334
|
+
node = node.value
|
|
335
|
+
return node.id if isinstance(node, ast.Name) else None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _bound_names(node: ast.AST) -> tuple[str, ...]:
|
|
339
|
+
match node:
|
|
340
|
+
case ast.Name(id=name, ctx=ast.Store() | ast.Del()) | ast.arg(arg=name):
|
|
341
|
+
return (name,)
|
|
342
|
+
case ast.alias():
|
|
343
|
+
return (node.asname or node.name.partition(".")[0],)
|
|
344
|
+
case (
|
|
345
|
+
ast.FunctionDef(name=name)
|
|
346
|
+
| ast.AsyncFunctionDef(name=name)
|
|
347
|
+
| ast.ClassDef(name=name)
|
|
348
|
+
| ast.ExceptHandler(name=str() as name)
|
|
349
|
+
| ast.MatchAs(name=str() as name)
|
|
350
|
+
| ast.MatchStar(name=str() as name)
|
|
351
|
+
| ast.MatchMapping(rest=str() as name)
|
|
352
|
+
):
|
|
353
|
+
return (name,)
|
|
354
|
+
case ast.Global(names=names) | ast.Nonlocal(names=names):
|
|
355
|
+
return tuple(names)
|
|
356
|
+
case _:
|
|
357
|
+
return ()
|
|
@@ -30,7 +30,7 @@ class AsyncCleanupRegisteredSynchronously(Rule):
|
|
|
30
30
|
id: str = "async-cleanup-registered-synchronously"
|
|
31
31
|
code: str = "SARJ463"
|
|
32
32
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
33
|
-
default_level=Severity.
|
|
33
|
+
default_level=Severity.ERROR,
|
|
34
34
|
summary="Do not register a proven async cleanup function with ExitStack.callback.",
|
|
35
35
|
rationale="The synchronous callback API discards the returned coroutine without awaiting it, so the cleanup body never runs.",
|
|
36
36
|
remediation="Use AsyncExitStack and register the coroutine function with push_async_callback.",
|
|
@@ -40,6 +40,8 @@ class AsyncCleanupRegisteredSynchronously(Rule):
|
|
|
40
40
|
"Requires a proven contextlib ExitStack or AsyncExitStack constructor and an undecorated, visible async function, optionally through simple function aliases.",
|
|
41
41
|
"Unknown methods/callables, decorated functions, async generators, conditional bindings, transferred stacks, wildcard imports, and generated files are excluded.",
|
|
42
42
|
"Resource aliases and interprocedural mutation are not inferred. No autofix is offered because executing previously dropped cleanup can raise new exceptions.",
|
|
43
|
+
"Coroutine-function assignment aliases must remain unmutated and unescaped; attribute inspection, containers, returned functions and unknown helper arguments abstain. Direct calls and proven stack registrations remain supported.",
|
|
44
|
+
"Visible namespace APIs, frame/module introspection, code mutation and bound attribute setters invalidate the file's provenance proof.",
|
|
43
45
|
),
|
|
44
46
|
examples=(
|
|
45
47
|
RuleExample(
|
|
@@ -70,7 +72,7 @@ class AsyncCleanupRegisteredSynchronously(Rule):
|
|
|
70
72
|
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
71
73
|
if "callback" not in context.symbol_source or context.generated or context.tree is None:
|
|
72
74
|
return []
|
|
73
|
-
provenance = ResourceProvenance(context)
|
|
75
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
74
76
|
findings: list[Diagnostic] = []
|
|
75
77
|
for call in context.nodes(ast.Call):
|
|
76
78
|
if not _wrong_callback(call, provenance) or is_suppressed(context.source_lines, call.lineno, self.code):
|
|
@@ -81,7 +83,7 @@ class AsyncCleanupRegisteredSynchronously(Rule):
|
|
|
81
83
|
line=call.lineno,
|
|
82
84
|
col=call.col_offset + 1,
|
|
83
85
|
code=self.code,
|
|
84
|
-
severity=Severity.
|
|
86
|
+
severity=Severity.ERROR,
|
|
85
87
|
message="synchronous ExitStack.callback does not await this async cleanup function; use AsyncExitStack.push_async_callback",
|
|
86
88
|
)
|
|
87
89
|
)
|
|
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
20
20
|
)
|
|
21
21
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
22
22
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
23
|
+
from sarj_python_lint.rules._pytest import collected_tests
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -224,24 +225,13 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
224
225
|
severity=Severity.WARNING,
|
|
225
226
|
message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
|
|
226
227
|
)
|
|
227
|
-
for test in
|
|
228
|
+
for test in collected_tests(tree.body)
|
|
228
229
|
if not _is_pytest_fixture(test, imports)
|
|
229
230
|
for call in _uncovered_calls(_test_imports(imports, test), test, writes)
|
|
230
231
|
if not is_suppressed(lines, call.lineno, self.code)
|
|
231
232
|
]
|
|
232
233
|
|
|
233
234
|
|
|
234
|
-
def _collected_tests(statements: list[ast.stmt]) -> Iterator[_TestFunction]:
|
|
235
|
-
for statement in statements:
|
|
236
|
-
match statement:
|
|
237
|
-
case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
|
|
238
|
-
yield statement
|
|
239
|
-
case ast.ClassDef() if statement.name.startswith("Test"):
|
|
240
|
-
yield from _collected_tests(statement.body)
|
|
241
|
-
case _:
|
|
242
|
-
pass
|
|
243
|
-
|
|
244
|
-
|
|
245
235
|
def _is_pytest_fixture(test: _TestFunction, imports: ImportIndex) -> bool:
|
|
246
236
|
return any(
|
|
247
237
|
imports.resolved_qualified_name(decorator.func if isinstance(decorator, ast.Call) else decorator)
|