sarj-python-lint 1.43.1__tar.gz → 1.44.1__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.1 → sarj_python_lint-1.44.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/pyproject.toml +1 -1
- sarj_python_lint-1.44.1/src/sarj_python_lint/rules/_type_guard_symbols.py +263 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +64 -13
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_partial_type_is.py +152 -20
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +18 -4
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +119 -28
- sarj_python_lint-1.43.1/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -112
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/.gitignore +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/LICENSE +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/README.md +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.43.1 → sarj_python_lint-1.44.1}/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.1
|
|
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,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
|
|
@@ -17,7 +17,11 @@ from sarj_python_lint.rule_base import (
|
|
|
17
17
|
is_suppressed,
|
|
18
18
|
)
|
|
19
19
|
from sarj_python_lint.rules._imports import TYPING_SOURCES
|
|
20
|
-
from sarj_python_lint.rules._type_guard_symbols import
|
|
20
|
+
from sarj_python_lint.rules._type_guard_symbols import (
|
|
21
|
+
builtin_guard_symbol,
|
|
22
|
+
guard_symbols_ambiguous,
|
|
23
|
+
unique_guard_function_names,
|
|
24
|
+
)
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
if TYPE_CHECKING:
|
|
@@ -33,7 +37,7 @@ _WITNESSES: Final = {
|
|
|
33
37
|
"str": frozenset({"str"}),
|
|
34
38
|
"bytes": frozenset({"bytes"}),
|
|
35
39
|
}
|
|
36
|
-
_ALL_WITNESSES = frozenset({"int", "bool", "str", "bytes"})
|
|
40
|
+
_ALL_WITNESSES = frozenset({"int", "bool", "str", "bytes", "none"})
|
|
37
41
|
_MAX_INPUT_NODES = 4096
|
|
38
42
|
_BAD = (
|
|
39
43
|
"from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n return isinstance(value, int)\n"
|
|
@@ -47,9 +51,9 @@ class NoContradictoryTypeGuard(Rule):
|
|
|
47
51
|
code = "SARJ513"
|
|
48
52
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
49
53
|
default_level=Severity.WARNING,
|
|
50
|
-
summary="Primitive type guards must agree with the
|
|
54
|
+
summary="Primitive type guards must agree with the builtin predicate they return.",
|
|
51
55
|
rationale=(
|
|
52
|
-
"A guard annotation does not validate its implementation. Returning an incompatible
|
|
56
|
+
"A guard annotation does not validate its implementation. Returning an incompatible primitive predicate "
|
|
53
57
|
"check makes callers trust the wrong primitive; TypeIs also incorrectly excludes rejected target "
|
|
54
58
|
"values in the false branch."
|
|
55
59
|
),
|
|
@@ -61,12 +65,14 @@ class NoContradictoryTypeGuard(Rule):
|
|
|
61
65
|
category=RuleCategory.CORRECTNESS,
|
|
62
66
|
autofix=AutofixPolicy.NONE,
|
|
63
67
|
limitations=(
|
|
64
|
-
"Checks undecorated, non-generic module functions with one parameter and a single returned direct builtin isinstance call, optionally after a docstring.",
|
|
68
|
+
"Checks undecorated, non-generic module functions with one parameter and a single returned direct builtin isinstance call, its unary not, or a bool constant, optionally after a docstring.",
|
|
65
69
|
"Guard symbols must resolve to typing or typing_extensions TypeGuard or TypeIs. Targets and checked classes are builtin int, bool, str or bytes; bool is treated as an int subtype.",
|
|
66
|
-
"Inputs must be object or fully classified plain unions of those primitive types and None.
|
|
70
|
+
"Inputs must be object or fully classified plain unions of those primitive types and None. Direct positive impossible isinstance tests and compiler-invalid TypeIs target/input relationships are excluded.",
|
|
67
71
|
"Input unions are traversed iteratively with a bounded node budget; overly complex annotations are excluded.",
|
|
68
72
|
"Literal and refined targets, aliases, custom classes, float numeric promotion, tuple isinstance tests, helper predicates, nested functions and complex bodies are not inferred.",
|
|
69
73
|
"TypeGuard may intentionally recognize a subset of its target, so TypeGuard[int] with an isinstance bool test remains legal. Visible namespace mutation and ambiguous or shadowed symbols abstain conservatively.",
|
|
74
|
+
"Constant false remains legal for TypeGuard. Constant true is reported only with a proven incompatible primitive or None input; false TypeIs needs a proven inhabited target. Negated isinstance uses the same finite witness domains.",
|
|
75
|
+
"Repeated top-level function definition names abstain because overloads can constrain the public input more narrowly than the implementation input.",
|
|
70
76
|
"Generated files are excluded; maintained tests remain checked. No autofix guesses a domain or changes false-branch behavior.",
|
|
71
77
|
),
|
|
72
78
|
examples=(
|
|
@@ -88,6 +94,36 @@ class NoContradictoryTypeGuard(Rule):
|
|
|
88
94
|
expected_count=0,
|
|
89
95
|
public=True,
|
|
90
96
|
),
|
|
97
|
+
RuleExample(
|
|
98
|
+
example_id="constant-true-claims-text",
|
|
99
|
+
scenario="constant-predicate",
|
|
100
|
+
title="A constant result does not prove a primitive type",
|
|
101
|
+
outcome=ExampleOutcome.MATCH,
|
|
102
|
+
files=(
|
|
103
|
+
ExampleFile.python(
|
|
104
|
+
"app/guards.py",
|
|
105
|
+
"from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n return True\n",
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
focus_path=PurePosixPath("app/guards.py"),
|
|
109
|
+
expected_count=1,
|
|
110
|
+
public=True,
|
|
111
|
+
),
|
|
112
|
+
RuleExample(
|
|
113
|
+
example_id="constant-false-is-legal-partial-guard",
|
|
114
|
+
scenario="constant-predicate",
|
|
115
|
+
title="TypeGuard can intentionally recognize no inputs",
|
|
116
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
117
|
+
files=(
|
|
118
|
+
ExampleFile.python(
|
|
119
|
+
"app/guards.py",
|
|
120
|
+
"from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n return False\n",
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
focus_path=PurePosixPath("app/guards.py"),
|
|
124
|
+
expected_count=0,
|
|
125
|
+
public=True,
|
|
126
|
+
),
|
|
91
127
|
),
|
|
92
128
|
)
|
|
93
129
|
description = documentation.summary
|
|
@@ -99,8 +135,9 @@ class NoContradictoryTypeGuard(Rule):
|
|
|
99
135
|
if context.tree is None or guard_symbols_ambiguous(context, _BUILTINS):
|
|
100
136
|
return []
|
|
101
137
|
findings: list[Diagnostic] = []
|
|
138
|
+
unique_names = unique_guard_function_names(context)
|
|
102
139
|
for function in context.tree.body:
|
|
103
|
-
if not isinstance(function, ast.FunctionDef):
|
|
140
|
+
if not isinstance(function, ast.FunctionDef) or function.name not in unique_names:
|
|
104
141
|
continue
|
|
105
142
|
statement = _contradictory_return(function, context)
|
|
106
143
|
if statement is None or is_suppressed(context.source_lines, statement.lineno, self.code):
|
|
@@ -111,7 +148,7 @@ class NoContradictoryTypeGuard(Rule):
|
|
|
111
148
|
statement.lineno,
|
|
112
149
|
statement.col_offset + 1,
|
|
113
150
|
self.code,
|
|
114
|
-
"This guard's
|
|
151
|
+
"This guard's builtin predicate contradicts its primitive narrowing promise; align the target "
|
|
115
152
|
"and test, preserving TypeIs false-branch behavior and intentional TypeGuard partial predicates.",
|
|
116
153
|
severity=Severity.WARNING,
|
|
117
154
|
)
|
|
@@ -133,12 +170,11 @@ def _contradictory_return(function: ast.FunctionDef, context: PythonFileContext)
|
|
|
133
170
|
if target is None or inputs is None or (guard == "TypeIs" and not target <= inputs):
|
|
134
171
|
return None
|
|
135
172
|
statement = _single_return(function)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if checked is None or not inputs & checked:
|
|
173
|
+
accepted = _accepted_witnesses(statement.value, parameter.arg, inputs, context) if statement is not None else None
|
|
174
|
+
if accepted is None:
|
|
139
175
|
return None
|
|
140
|
-
accepts_wrong = bool(
|
|
141
|
-
rejects_target = guard == "TypeIs" and bool((inputs & target) -
|
|
176
|
+
accepts_wrong = bool(accepted - target)
|
|
177
|
+
rejects_target = guard == "TypeIs" and bool((inputs & target) - accepted)
|
|
142
178
|
return statement if accepts_wrong or rejects_target else None
|
|
143
179
|
|
|
144
180
|
|
|
@@ -174,6 +210,7 @@ def _input_witnesses(annotation: ast.expr | None, imports: ImportIndex) -> froze
|
|
|
174
210
|
if visited > _MAX_INPUT_NODES:
|
|
175
211
|
return None
|
|
176
212
|
if isinstance(member, ast.Constant) and member.value is None:
|
|
213
|
+
witnesses.add("none")
|
|
177
214
|
continue
|
|
178
215
|
if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
|
|
179
216
|
pending.extend((member.left, member.right))
|
|
@@ -188,6 +225,20 @@ def _input_witnesses(annotation: ast.expr | None, imports: ImportIndex) -> froze
|
|
|
188
225
|
return frozenset(witnesses)
|
|
189
226
|
|
|
190
227
|
|
|
228
|
+
def _accepted_witnesses(
|
|
229
|
+
node: ast.expr | None, parameter: str, inputs: frozenset[str], context: PythonFileContext
|
|
230
|
+
) -> frozenset[str] | None:
|
|
231
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, bool):
|
|
232
|
+
return inputs if node.value else frozenset()
|
|
233
|
+
negate = isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not)
|
|
234
|
+
expression = node.operand if negate and isinstance(node, ast.UnaryOp) else node
|
|
235
|
+
checked_kind = _checked_kind(expression, parameter, context)
|
|
236
|
+
checked = _WITNESSES.get(checked_kind) if checked_kind is not None else None
|
|
237
|
+
if checked is None or (not negate and not inputs & checked):
|
|
238
|
+
return None
|
|
239
|
+
return inputs - checked if negate else inputs & checked
|
|
240
|
+
|
|
241
|
+
|
|
191
242
|
def _checked_kind(node: ast.expr | None, parameter: str, context: PythonFileContext) -> str | None:
|
|
192
243
|
match node:
|
|
193
244
|
case ast.Call(func=func, args=[ast.Name(id=name), target], keywords=[]) if name == parameter:
|