sarj-python-lint 1.43.0__tar.gz → 1.44.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.43.0 → sarj_python_lint-1.44.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_resource_provenance.py +10 -6
- sarj_python_lint-1.44.0/src/sarj_python_lint/rules/_type_guard_symbols.py +263 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +100 -24
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +64 -13
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_partial_type_is.py +152 -20
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +18 -4
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +157 -23
- sarj_python_lint-1.43.0/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -112
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/.gitignore +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/LICENSE +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/README.md +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.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.44.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/
|
|
@@ -56,6 +56,15 @@ _RESOURCE_OWNERS = MappingProxyType(
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def builtin_namespace_untouched(context: PythonFileContext) -> bool:
|
|
60
|
+
return not any(
|
|
61
|
+
(isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
|
|
62
|
+
or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
|
|
63
|
+
or (isinstance(node, ast.Name) and node.id == "__builtins__")
|
|
64
|
+
for node in context.nodes(ast.Import, ast.ImportFrom, ast.Name)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
59
68
|
@final
|
|
60
69
|
class ResourceProvenance:
|
|
61
70
|
def __init__(self, context: PythonFileContext, *, require_static_namespace: bool = False) -> None:
|
|
@@ -86,12 +95,7 @@ class ResourceProvenance:
|
|
|
86
95
|
|
|
87
96
|
@cached_property
|
|
88
97
|
def builtin_namespace_untouched(self) -> bool:
|
|
89
|
-
return
|
|
90
|
-
(isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
|
|
91
|
-
or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
|
|
92
|
-
or (isinstance(node, ast.Name) and node.id == "__builtins__")
|
|
93
|
-
for node in self.context.nodes(ast.Import, ast.ImportFrom, ast.Name)
|
|
94
|
-
)
|
|
98
|
+
return builtin_namespace_untouched(self.context)
|
|
95
99
|
|
|
96
100
|
def scope(self, node: ast.AST) -> ast.AST | None:
|
|
97
101
|
parent = self.context.parents.get(node)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rules._imports import TYPING_SOURCES
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
12
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_NAMESPACE_IMPORT_CAPABILITIES = frozenset(
|
|
16
|
+
{"__dict__", "__builtins__", "__getattribute__", "__setattr__", "__delattr__"}
|
|
17
|
+
)
|
|
18
|
+
_NAMESPACE_RETURNING_FUNCTIONS = frozenset({"__lazy_import__"})
|
|
19
|
+
_BUILTIN_NAMESPACE_FUNCTIONS = frozenset(
|
|
20
|
+
{
|
|
21
|
+
"__build_class__",
|
|
22
|
+
"__import__",
|
|
23
|
+
"__lazy_import__",
|
|
24
|
+
"abs",
|
|
25
|
+
"aiter",
|
|
26
|
+
"all",
|
|
27
|
+
"anext",
|
|
28
|
+
"any",
|
|
29
|
+
"ascii",
|
|
30
|
+
"bin",
|
|
31
|
+
"breakpoint",
|
|
32
|
+
"callable",
|
|
33
|
+
"chr",
|
|
34
|
+
"compile",
|
|
35
|
+
"delattr",
|
|
36
|
+
"dir",
|
|
37
|
+
"divmod",
|
|
38
|
+
"eval",
|
|
39
|
+
"exec",
|
|
40
|
+
"format",
|
|
41
|
+
"getattr",
|
|
42
|
+
"globals",
|
|
43
|
+
"hasattr",
|
|
44
|
+
"hash",
|
|
45
|
+
"hex",
|
|
46
|
+
"id",
|
|
47
|
+
"input",
|
|
48
|
+
"isinstance",
|
|
49
|
+
"issubclass",
|
|
50
|
+
"iter",
|
|
51
|
+
"len",
|
|
52
|
+
"locals",
|
|
53
|
+
"max",
|
|
54
|
+
"min",
|
|
55
|
+
"next",
|
|
56
|
+
"oct",
|
|
57
|
+
"ord",
|
|
58
|
+
"pow",
|
|
59
|
+
"print",
|
|
60
|
+
"repr",
|
|
61
|
+
"round",
|
|
62
|
+
"setattr",
|
|
63
|
+
"sorted",
|
|
64
|
+
"sum",
|
|
65
|
+
"vars",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def builtin_guard_symbol(node: ast.expr | None, imports: ImportIndex, builtin_symbols: frozenset[str]) -> str | None:
|
|
71
|
+
if node is None:
|
|
72
|
+
return None
|
|
73
|
+
symbol = imports.resolved_symbol(node, sources=frozenset({"builtins"}))
|
|
74
|
+
if symbol in builtin_symbols:
|
|
75
|
+
return symbol
|
|
76
|
+
if isinstance(node, ast.Name) and node.id in builtin_symbols and imports.builtin_is_unshadowed(node.id):
|
|
77
|
+
return node.id
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def unique_guard_function_names(context: PythonFileContext) -> frozenset[str]:
|
|
82
|
+
if context.tree is None:
|
|
83
|
+
return frozenset()
|
|
84
|
+
counts: Counter[str] = Counter()
|
|
85
|
+
pending: list[ast.AST] = list(context.tree.body)
|
|
86
|
+
while pending:
|
|
87
|
+
node = pending.pop()
|
|
88
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
89
|
+
counts[node.name] += 1
|
|
90
|
+
elif not isinstance(node, (ast.ClassDef, ast.Lambda)):
|
|
91
|
+
pending.extend(ast.iter_child_nodes(node))
|
|
92
|
+
return frozenset(name for name, count in counts.items() if count == 1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def guard_symbols_ambiguous(
|
|
96
|
+
context: PythonFileContext,
|
|
97
|
+
builtin_symbols: frozenset[str],
|
|
98
|
+
*,
|
|
99
|
+
typing_symbols: frozenset[str] = frozenset({"TypeGuard", "TypeIs"}),
|
|
100
|
+
) -> bool:
|
|
101
|
+
namespace_names = _namespace_import_names(context)
|
|
102
|
+
if _namespace_escapes(context, namespace_names):
|
|
103
|
+
return True
|
|
104
|
+
mutation_targets = (
|
|
105
|
+
{f"builtins.{symbol}" for symbol in builtin_symbols}
|
|
106
|
+
| {f"{module}.{symbol}" for module in TYPING_SOURCES for symbol in typing_symbols}
|
|
107
|
+
| {f"{module}.__class__" for module in TYPING_SOURCES | {"builtins"}}
|
|
108
|
+
)
|
|
109
|
+
sensitive_names = (
|
|
110
|
+
builtin_symbols
|
|
111
|
+
| namespace_names
|
|
112
|
+
| {
|
|
113
|
+
name
|
|
114
|
+
for name, target in context.module_imports.bindings.items()
|
|
115
|
+
if target.module in TYPING_SOURCES | {"builtins"}
|
|
116
|
+
and (target.symbol is None or target.symbol in builtin_symbols | typing_symbols)
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
return any(
|
|
120
|
+
_ambiguous_node(node, context, mutation_targets, sensitive_names)
|
|
121
|
+
for node in context.nodes(
|
|
122
|
+
ast.Import,
|
|
123
|
+
ast.ImportFrom,
|
|
124
|
+
ast.Attribute,
|
|
125
|
+
ast.ExceptHandler,
|
|
126
|
+
ast.MatchAs,
|
|
127
|
+
ast.MatchStar,
|
|
128
|
+
ast.MatchMapping,
|
|
129
|
+
ast.NamedExpr,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _namespace_import_names(context: PythonFileContext) -> frozenset[str]:
|
|
135
|
+
return frozenset(
|
|
136
|
+
alias.asname or alias.name
|
|
137
|
+
for node in context.nodes(ast.Import)
|
|
138
|
+
for alias in node.names
|
|
139
|
+
if alias.name in TYPING_SOURCES | {"builtins"}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _namespace_escapes(context: PythonFileContext, namespace_names: frozenset[str]) -> bool:
|
|
144
|
+
if context.dynamic_namespace or any(node.id == "__builtins__" for node in context.nodes(ast.Name)):
|
|
145
|
+
return True
|
|
146
|
+
if any(
|
|
147
|
+
node.level == 0
|
|
148
|
+
and node.module in TYPING_SOURCES | {"builtins"}
|
|
149
|
+
and any(alias.name in _NAMESPACE_IMPORT_CAPABILITIES for alias in node.names)
|
|
150
|
+
for node in context.nodes(ast.ImportFrom)
|
|
151
|
+
):
|
|
152
|
+
return True
|
|
153
|
+
if _native_function_escapes(context):
|
|
154
|
+
return True
|
|
155
|
+
for name in context.nodes(ast.Name):
|
|
156
|
+
if name.id not in namespace_names:
|
|
157
|
+
continue
|
|
158
|
+
parent = context.parents.get(name)
|
|
159
|
+
if not isinstance(parent, ast.Attribute) or parent.value is not name or parent.attr == "__dict__":
|
|
160
|
+
return True
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _native_function_escapes(context: PythonFileContext) -> bool:
|
|
165
|
+
function_names = _possible_native_function_names(context)
|
|
166
|
+
namespace_function_names = _possible_native_function_names(context, symbols=_NAMESPACE_RETURNING_FUNCTIONS)
|
|
167
|
+
module_names = frozenset(
|
|
168
|
+
alias.asname or alias.name
|
|
169
|
+
for node in context.nodes(ast.Import)
|
|
170
|
+
for alias in node.names
|
|
171
|
+
if alias.name == "builtins"
|
|
172
|
+
)
|
|
173
|
+
for node in context.nodes(ast.Name, ast.Attribute):
|
|
174
|
+
if isinstance(node, ast.Attribute) and (
|
|
175
|
+
not isinstance(node.value, ast.Name) or node.attr not in _BUILTIN_NAMESPACE_FUNCTIONS
|
|
176
|
+
):
|
|
177
|
+
continue
|
|
178
|
+
if not isinstance(node.ctx, ast.Load) or not _possible_native_function(node, function_names, module_names):
|
|
179
|
+
continue
|
|
180
|
+
parent = context.parents.get(node)
|
|
181
|
+
if (
|
|
182
|
+
not isinstance(parent, ast.Call)
|
|
183
|
+
or parent.func is not node
|
|
184
|
+
or _namespace_returning_function(node, namespace_function_names)
|
|
185
|
+
):
|
|
186
|
+
return True
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _possible_native_function_names(
|
|
191
|
+
context: PythonFileContext, *, symbols: frozenset[str] = _BUILTIN_NAMESPACE_FUNCTIONS
|
|
192
|
+
) -> frozenset[str]:
|
|
193
|
+
return symbols | frozenset(
|
|
194
|
+
alias.asname or alias.name
|
|
195
|
+
for node in context.nodes(ast.ImportFrom)
|
|
196
|
+
if node.level == 0 and node.module == "builtins"
|
|
197
|
+
for alias in node.names
|
|
198
|
+
if alias.name in symbols
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _namespace_returning_function(node: ast.Name | ast.Attribute, function_names: frozenset[str]) -> bool:
|
|
203
|
+
if isinstance(node, ast.Name):
|
|
204
|
+
return node.id in function_names
|
|
205
|
+
return node.attr in _NAMESPACE_RETURNING_FUNCTIONS
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _possible_native_function(
|
|
209
|
+
node: ast.Name | ast.Attribute,
|
|
210
|
+
function_names: frozenset[str],
|
|
211
|
+
module_names: frozenset[str],
|
|
212
|
+
) -> bool:
|
|
213
|
+
if isinstance(node, ast.Name):
|
|
214
|
+
return node.id in function_names
|
|
215
|
+
return (
|
|
216
|
+
isinstance(node.value, ast.Name) and node.value.id in module_names and node.attr in _BUILTIN_NAMESPACE_FUNCTIONS
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _ambiguous_node(
|
|
221
|
+
node: ast.AST, context: PythonFileContext, mutation_targets: set[str], sensitive_names: frozenset[str]
|
|
222
|
+
) -> bool:
|
|
223
|
+
match node:
|
|
224
|
+
case ast.Import() | ast.ImportFrom():
|
|
225
|
+
return _ambiguous_import(node, context.module_imports, sensitive_names)
|
|
226
|
+
case ast.Attribute(ctx=(ast.Store() | ast.Del())):
|
|
227
|
+
return context.module_imports.resolved_qualified_name(node) in mutation_targets
|
|
228
|
+
case (
|
|
229
|
+
ast.ExceptHandler(name=name)
|
|
230
|
+
| ast.MatchAs(name=name)
|
|
231
|
+
| ast.MatchStar(name=name)
|
|
232
|
+
| ast.MatchMapping(rest=name)
|
|
233
|
+
| ast.NamedExpr(target=ast.Name(id=name))
|
|
234
|
+
):
|
|
235
|
+
return name in sensitive_names
|
|
236
|
+
case _:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _ambiguous_import(node: ast.Import | ast.ImportFrom, imports: ImportIndex, sensitive_names: frozenset[str]) -> bool:
|
|
241
|
+
if isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names):
|
|
242
|
+
return True
|
|
243
|
+
for alias in node.names:
|
|
244
|
+
name = alias.asname or alias.name.partition(".")[0]
|
|
245
|
+
if name not in sensitive_names:
|
|
246
|
+
continue
|
|
247
|
+
if not _preserves_canonical_import(node, alias, imports):
|
|
248
|
+
return True
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _preserves_canonical_import(node: ast.Import | ast.ImportFrom, alias: ast.alias, imports: ImportIndex) -> bool:
|
|
253
|
+
if isinstance(node, ast.ImportFrom):
|
|
254
|
+
if node.level:
|
|
255
|
+
return False
|
|
256
|
+
module, symbol = node.module, alias.name
|
|
257
|
+
else:
|
|
258
|
+
module, symbol = alias.name, None
|
|
259
|
+
name = alias.asname or alias.name.partition(".")[0]
|
|
260
|
+
target = imports.bindings.get(name)
|
|
261
|
+
if target is None:
|
|
262
|
+
return module == "builtins" and symbol == name
|
|
263
|
+
return target.module == module and target.symbol == symbol
|
|
@@ -21,7 +21,7 @@ from sarj_python_lint.rule_base import (
|
|
|
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
23
|
from sarj_python_lint.rules._pytest import collected_tests
|
|
24
|
-
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
24
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance, builtin_namespace_untouched
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
@@ -97,13 +97,44 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
97
97
|
"Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
|
|
98
98
|
"Visible provider replacement, escape or unresolved lexical ownership retains WARNING; only the closed canonical AsyncMock provider proof blocks.",
|
|
99
99
|
"Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
|
|
100
|
-
"Positive call evidence includes direct assertion methods and literal
|
|
101
|
-
"Await evidence excludes provably vacuous checks, including zero or nonnegative
|
|
100
|
+
"Positive call evidence includes direct assertion methods and literal numeric or boolean call-state checks; unresolved call expectations are excluded.",
|
|
101
|
+
"Await evidence excludes provably vacuous checks, including non-None count/list state, zero or nonnegative numeric bounds, and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
|
|
102
|
+
"Length comparisons over await_args_list use count semantics only for an unshadowed builtin len without visible builtin namespace access; custom length functions remain unresolved.",
|
|
102
103
|
"Conjunctions need one accepted await branch; disjunctions need accepted evidence for the same mock in every branch. Argument equality and assertion strength are not compared.",
|
|
103
104
|
"Compound negation and custom expression semantics remain unresolved; no variable values or fixture graphs are inferred.",
|
|
104
105
|
"Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
|
|
105
106
|
),
|
|
106
107
|
examples=(
|
|
108
|
+
RuleExample(
|
|
109
|
+
example_id="vacuous-await-length",
|
|
110
|
+
scenario="await-length-evidence",
|
|
111
|
+
title="An empty await history still has nonnegative length",
|
|
112
|
+
outcome=ExampleOutcome.MATCH,
|
|
113
|
+
files=(
|
|
114
|
+
ExampleFile.python(
|
|
115
|
+
"tests/test_delivery.py",
|
|
116
|
+
"from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n assert len(send.await_args_list) >= 0\n",
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
120
|
+
expected_count=1,
|
|
121
|
+
public=True,
|
|
122
|
+
),
|
|
123
|
+
RuleExample(
|
|
124
|
+
example_id="positive-await-length",
|
|
125
|
+
scenario="await-length-evidence",
|
|
126
|
+
title="A positive await-history length distinguishes a dropped coroutine",
|
|
127
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
128
|
+
files=(
|
|
129
|
+
ExampleFile.python(
|
|
130
|
+
"tests/test_delivery.py",
|
|
131
|
+
"from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n assert len(send.await_args_list) == 1\n",
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
focus_path=PurePosixPath("tests/test_delivery.py"),
|
|
135
|
+
expected_count=0,
|
|
136
|
+
public=True,
|
|
137
|
+
),
|
|
107
138
|
RuleExample(
|
|
108
139
|
example_id="spaced-positive-called-state",
|
|
109
140
|
scenario="called-attribute-spacing",
|
|
@@ -212,6 +243,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
212
243
|
if tree is None:
|
|
213
244
|
return []
|
|
214
245
|
imports = context.module_imports
|
|
246
|
+
builtin_len = builtin_namespace_untouched(context)
|
|
215
247
|
writes = {
|
|
216
248
|
_reference(node)
|
|
217
249
|
for node in context.nodes(ast.AST)
|
|
@@ -221,14 +253,20 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
221
253
|
finding
|
|
222
254
|
for test in collected_tests(tree.body)
|
|
223
255
|
if not _is_pytest_fixture(test, imports)
|
|
224
|
-
for finding in _test_diagnostics(test, context, imports, writes, self.code)
|
|
256
|
+
for finding in _test_diagnostics(test, context, imports, writes, self.code, builtin_len=builtin_len)
|
|
225
257
|
]
|
|
226
258
|
|
|
227
259
|
|
|
228
260
|
def _test_diagnostics(
|
|
229
|
-
test: _TestFunction,
|
|
261
|
+
test: _TestFunction,
|
|
262
|
+
context: PythonFileContext,
|
|
263
|
+
imports: ImportIndex,
|
|
264
|
+
writes: set[_Reference],
|
|
265
|
+
code: str,
|
|
266
|
+
*,
|
|
267
|
+
builtin_len: bool,
|
|
230
268
|
) -> list[Diagnostic]:
|
|
231
|
-
calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes))
|
|
269
|
+
calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes, builtin_len=builtin_len))
|
|
232
270
|
if not calls:
|
|
233
271
|
return []
|
|
234
272
|
severity = Severity.ERROR if _mock_provider_proven(test, context) else Severity.WARNING
|
|
@@ -281,11 +319,11 @@ def _test_imports(module: ImportIndex, test: _TestFunction) -> ImportIndex:
|
|
|
281
319
|
|
|
282
320
|
|
|
283
321
|
def _uncovered_calls(
|
|
284
|
-
imports: ImportIndex, test: _TestFunction, writes: set[_Reference]
|
|
322
|
+
imports: ImportIndex, test: _TestFunction, writes: set[_Reference], *, builtin_len: bool
|
|
285
323
|
) -> Iterator[ast.Call | ast.Attribute]:
|
|
286
324
|
nodes = tuple(walk_ast(test))
|
|
287
325
|
bindings = _mock_bindings(imports, test.body, writes)
|
|
288
|
-
covered = _await_oracles(test.body)
|
|
326
|
+
covered = _await_oracles(test.body, builtin_len=builtin_len and imports.builtin_is_unshadowed("len"))
|
|
289
327
|
reported: set[_Reference] = set()
|
|
290
328
|
for statement in test.body:
|
|
291
329
|
for reference, oracle in _call_oracles(statement):
|
|
@@ -375,7 +413,7 @@ def _is_async_reference(reference: _Reference, constructor: ast.Call) -> bool:
|
|
|
375
413
|
)
|
|
376
414
|
|
|
377
415
|
|
|
378
|
-
def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
|
|
416
|
+
def _await_oracles(statements: list[ast.stmt], *, builtin_len: bool) -> set[_Reference]:
|
|
379
417
|
covered: set[_Reference] = set()
|
|
380
418
|
for statement in statements:
|
|
381
419
|
match statement:
|
|
@@ -385,7 +423,7 @@ def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
|
|
|
385
423
|
if method != "assert_has_awaits" or _calls_evidence(call) is not _Evidence.INSUFFICIENT:
|
|
386
424
|
covered.add(_reference(receiver))
|
|
387
425
|
case ast.Assert(test=condition):
|
|
388
|
-
covered.update(_asserted_states(condition, _AWAIT_STATE))
|
|
426
|
+
covered.update(_asserted_states(condition, _AWAIT_STATE, builtin_len=builtin_len))
|
|
389
427
|
case _:
|
|
390
428
|
pass
|
|
391
429
|
return covered
|
|
@@ -409,17 +447,21 @@ def _sequence_evidence(node: ast.expr) -> _Evidence:
|
|
|
409
447
|
return _Evidence.UNRESOLVED
|
|
410
448
|
|
|
411
449
|
|
|
412
|
-
def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Reference]:
|
|
450
|
+
def _asserted_states(condition: ast.expr, states: frozenset[str], *, builtin_len: bool = False) -> set[_Reference]:
|
|
413
451
|
match condition:
|
|
414
452
|
case ast.UnaryOp(op=ast.Not(), operand=ast.UnaryOp(op=ast.Not(), operand=operand)):
|
|
415
|
-
return _asserted_states(operand, states)
|
|
453
|
+
return _asserted_states(operand, states, builtin_len=builtin_len)
|
|
416
454
|
case ast.UnaryOp(op=ast.Not(), operand=ast.Compare(left=left, ops=[operator], comparators=[right])):
|
|
417
455
|
inverse = _negated_operator(operator)
|
|
418
|
-
return
|
|
456
|
+
return (
|
|
457
|
+
_comparison_states(left, inverse, right, states, builtin_len=builtin_len)
|
|
458
|
+
if inverse is not None
|
|
459
|
+
else set()
|
|
460
|
+
)
|
|
419
461
|
case ast.UnaryOp(op=ast.Not(), operand=ast.Attribute(attr=attribute)) if attribute in states:
|
|
420
462
|
return set()
|
|
421
463
|
case ast.BoolOp(op=operator, values=values):
|
|
422
|
-
branches = [_asserted_states(value, states) for value in values]
|
|
464
|
+
branches = [_asserted_states(value, states, builtin_len=builtin_len) for value in values]
|
|
423
465
|
return (
|
|
424
466
|
branches[0].union(*branches[1:])
|
|
425
467
|
if isinstance(operator, ast.And)
|
|
@@ -430,7 +472,7 @@ def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Refere
|
|
|
430
472
|
case ast.Compare(left=left, ops=operators, comparators=rights):
|
|
431
473
|
result: set[_Reference] = set()
|
|
432
474
|
for first, operator, second in zip((left, *rights[:-1]), operators, rights, strict=True):
|
|
433
|
-
result.update(_comparison_states(first, operator, second, states))
|
|
475
|
+
result.update(_comparison_states(first, operator, second, states, builtin_len=builtin_len))
|
|
434
476
|
return result
|
|
435
477
|
case _:
|
|
436
478
|
# Preserve acceptance of unresolved await-state assertions, such as
|
|
@@ -453,18 +495,37 @@ def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
|
|
|
453
495
|
return factory() if factory is not None else None
|
|
454
496
|
|
|
455
497
|
|
|
456
|
-
def _comparison_states(
|
|
498
|
+
def _comparison_states(
|
|
499
|
+
left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str], *, builtin_len: bool = False
|
|
500
|
+
) -> set[_Reference]:
|
|
457
501
|
result: set[_Reference] = set()
|
|
458
502
|
for node, comparison, expected in ((left, operator, right), (right, _reversed_operator(operator), left)):
|
|
459
|
-
|
|
460
|
-
|
|
503
|
+
state = _state_operand(node, states, builtin_len=builtin_len)
|
|
504
|
+
if state is not None:
|
|
505
|
+
evidence = _state_evidence(
|
|
506
|
+
"await_count" if isinstance(node, ast.Call) else state.attr, comparison, expected
|
|
507
|
+
)
|
|
461
508
|
if evidence is _Evidence.POSITIVE or (states == _AWAIT_STATE and evidence is _Evidence.UNRESOLVED):
|
|
462
|
-
result.add(_reference(
|
|
509
|
+
result.add(_reference(state.value))
|
|
463
510
|
else:
|
|
464
511
|
result.update(_unresolved_await_states(node, states))
|
|
465
512
|
return result
|
|
466
513
|
|
|
467
514
|
|
|
515
|
+
def _state_operand(node: ast.expr, states: frozenset[str], *, builtin_len: bool) -> ast.Attribute | None:
|
|
516
|
+
if isinstance(node, ast.Attribute) and node.attr in states:
|
|
517
|
+
return node
|
|
518
|
+
if builtin_len and states == _AWAIT_STATE:
|
|
519
|
+
match node:
|
|
520
|
+
case ast.Call(
|
|
521
|
+
func=ast.Name(id="len"), args=[ast.Attribute(attr="await_args_list") as argument], keywords=[]
|
|
522
|
+
):
|
|
523
|
+
return argument
|
|
524
|
+
case _:
|
|
525
|
+
pass
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
468
529
|
def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
|
|
469
530
|
match operator:
|
|
470
531
|
case ast.Lt():
|
|
@@ -489,12 +550,14 @@ def _unresolved_await_states(node: ast.expr, states: frozenset[str]) -> set[_Ref
|
|
|
489
550
|
|
|
490
551
|
def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) -> _Evidence:
|
|
491
552
|
positive: bool
|
|
492
|
-
if (
|
|
493
|
-
|
|
553
|
+
if attribute in {"call_count", "await_count"} and (value := _literal_number(expected)) is not None:
|
|
554
|
+
positive = _proves_positive_count(operator, value)
|
|
555
|
+
elif (
|
|
556
|
+
attribute in {"call_count", "await_count", "await_args_list"}
|
|
494
557
|
and isinstance(expected, ast.Constant)
|
|
495
|
-
and
|
|
558
|
+
and expected.value is None
|
|
496
559
|
):
|
|
497
|
-
|
|
560
|
+
return _Evidence.INSUFFICIENT
|
|
498
561
|
elif attribute == "called" and isinstance(expected, ast.Constant) and isinstance(expected.value, bool):
|
|
499
562
|
positive = (isinstance(operator, (ast.Eq, ast.Is)) and expected.value) or (
|
|
500
563
|
isinstance(operator, (ast.NotEq, ast.IsNot)) and not expected.value
|
|
@@ -513,7 +576,20 @@ def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) ->
|
|
|
513
576
|
return _Evidence.POSITIVE if positive else _Evidence.INSUFFICIENT
|
|
514
577
|
|
|
515
578
|
|
|
516
|
-
def
|
|
579
|
+
def _literal_number(node: ast.expr) -> int | float | None:
|
|
580
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
|
581
|
+
return node.value
|
|
582
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant):
|
|
583
|
+
value = node.operand.value
|
|
584
|
+
if isinstance(value, (int, float)):
|
|
585
|
+
if isinstance(node.op, ast.USub):
|
|
586
|
+
return -value
|
|
587
|
+
if isinstance(node.op, ast.UAdd):
|
|
588
|
+
return +value
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _proves_positive_count(operator: ast.cmpop, value: float) -> bool:
|
|
517
593
|
match operator:
|
|
518
594
|
case ast.Eq() | ast.GtE():
|
|
519
595
|
return value > 0
|