sarj-python-lint 1.60.0__tar.gz → 1.61.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.60.0 → sarj_python_lint-1.61.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +95 -11
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_mutating_container_transform.py +2 -2
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +145 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +110 -14
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_str_enum.py +100 -6
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/.gitignore +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/LICENSE +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/README.md +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.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.61.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/
|
|
@@ -117,6 +117,34 @@ class NoAnyMappingTypes(Rule):
|
|
|
117
117
|
expected_count=0,
|
|
118
118
|
public=True,
|
|
119
119
|
),
|
|
120
|
+
RuleExample(
|
|
121
|
+
example_id="generic-any-binding",
|
|
122
|
+
scenario="provider-identity",
|
|
123
|
+
title="A generic parameter named Any keeps its own identity",
|
|
124
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
125
|
+
files=(
|
|
126
|
+
ExampleFile.python(
|
|
127
|
+
"app/service.py", "from typing import Any\nclass Rows[Any]:\n rows: dict[str, Any]\n"
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
131
|
+
expected_count=0,
|
|
132
|
+
public=True,
|
|
133
|
+
),
|
|
134
|
+
RuleExample(
|
|
135
|
+
example_id="generic-typing-any",
|
|
136
|
+
scenario="provider-identity",
|
|
137
|
+
title="An unrelated generic parameter preserves actual Any erasure",
|
|
138
|
+
outcome=ExampleOutcome.MATCH,
|
|
139
|
+
files=(
|
|
140
|
+
ExampleFile.python(
|
|
141
|
+
"app/service.py", "from typing import Any\nclass Rows[T]:\n rows: dict[str, Any]\n"
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
145
|
+
expected_count=1,
|
|
146
|
+
public=True,
|
|
147
|
+
),
|
|
120
148
|
),
|
|
121
149
|
)
|
|
122
150
|
description = documentation.summary
|
|
@@ -236,11 +264,12 @@ def _unshadowed_any_mappings(
|
|
|
236
264
|
if parsed is None:
|
|
237
265
|
return []
|
|
238
266
|
names = {node.id for node in walk_ast(parsed) if isinstance(node, ast.Name)}
|
|
239
|
-
|
|
267
|
+
type_parameters = frozenset(_generic_scope_names(owner, parents))
|
|
268
|
+
if not names.isdisjoint(_shadowed_names(owner, parents, scope_cache) - type_parameters):
|
|
240
269
|
return []
|
|
241
270
|
return [
|
|
242
271
|
(mapping, expression if isinstance(expression, ast.Constant) else mapping)
|
|
243
|
-
for mapping in _any_mappings(expression, semantics)
|
|
272
|
+
for mapping in _any_mappings(expression, semantics, shadowed=type_parameters)
|
|
244
273
|
]
|
|
245
274
|
|
|
246
275
|
|
|
@@ -546,12 +575,39 @@ def _unshadowed_type_expression(
|
|
|
546
575
|
return names.isdisjoint(_shadowed_names(node, parents, scope_cache))
|
|
547
576
|
|
|
548
577
|
|
|
578
|
+
def _generic_scope_names(node: ast.AST, parents: dict[int, ast.AST]) -> set[str]:
|
|
579
|
+
names = _type_parameter_names(node)
|
|
580
|
+
below = node
|
|
581
|
+
parent = parents.get(id(below))
|
|
582
|
+
function_default = False
|
|
583
|
+
while parent is not None:
|
|
584
|
+
match parent:
|
|
585
|
+
case ast.arguments():
|
|
586
|
+
function_default = below in parent.defaults or below in parent.kw_defaults
|
|
587
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
588
|
+
if not function_default and below not in parent.decorator_list:
|
|
589
|
+
names.update(_type_parameter_names(parent))
|
|
590
|
+
function_default = False
|
|
591
|
+
case ast.Lambda():
|
|
592
|
+
function_default = False
|
|
593
|
+
case ast.ClassDef():
|
|
594
|
+
if below not in parent.decorator_list:
|
|
595
|
+
names.update(_type_parameter_names(parent))
|
|
596
|
+
case ast.TypeAlias():
|
|
597
|
+
names.update(_type_parameter_names(parent))
|
|
598
|
+
case _:
|
|
599
|
+
pass
|
|
600
|
+
below = parent
|
|
601
|
+
parent = parents.get(id(below))
|
|
602
|
+
return names
|
|
603
|
+
|
|
604
|
+
|
|
549
605
|
def _shadowed_names(
|
|
550
606
|
node: ast.AST,
|
|
551
607
|
parents: dict[int, ast.AST],
|
|
552
608
|
scope_cache: dict[int, frozenset[str]],
|
|
553
609
|
) -> set[str]:
|
|
554
|
-
shadowed
|
|
610
|
+
shadowed = _generic_scope_names(node, parents)
|
|
555
611
|
crossed_function_body = False
|
|
556
612
|
below = node
|
|
557
613
|
parent = parents.get(id(below))
|
|
@@ -567,6 +623,16 @@ def _shadowed_names(
|
|
|
567
623
|
return shadowed
|
|
568
624
|
|
|
569
625
|
|
|
626
|
+
def _type_parameter_names(node: ast.AST) -> set[str]:
|
|
627
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.TypeAlias)):
|
|
628
|
+
return {
|
|
629
|
+
parameter.name
|
|
630
|
+
for parameter in node.type_params
|
|
631
|
+
if isinstance(parameter, (ast.TypeVar, ast.ParamSpec, ast.TypeVarTuple))
|
|
632
|
+
}
|
|
633
|
+
return set()
|
|
634
|
+
|
|
635
|
+
|
|
570
636
|
def _scope_names(
|
|
571
637
|
owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
|
|
572
638
|
cache: dict[int, frozenset[str]],
|
|
@@ -628,7 +694,9 @@ def _type_comment(comment: str, owner: ast.AST) -> ast.expr | None:
|
|
|
628
694
|
return expression
|
|
629
695
|
|
|
630
696
|
|
|
631
|
-
def _any_mappings(
|
|
697
|
+
def _any_mappings(
|
|
698
|
+
annotation: ast.expr, semantics: AnnotationSemantics, *, shadowed: frozenset[str] = frozenset()
|
|
699
|
+
) -> list[ast.Subscript]:
|
|
632
700
|
resolved = semantics.parse(annotation)
|
|
633
701
|
if resolved is None:
|
|
634
702
|
return []
|
|
@@ -636,24 +704,30 @@ def _any_mappings(annotation: ast.expr, semantics: AnnotationSemantics) -> list[
|
|
|
636
704
|
mappings: list[ast.Subscript] = []
|
|
637
705
|
while pending:
|
|
638
706
|
node = pending.pop()
|
|
639
|
-
if isinstance(node, ast.Subscript) and _is_any_mapping(node, semantics):
|
|
707
|
+
if isinstance(node, ast.Subscript) and _is_any_mapping(node, semantics, shadowed=shadowed):
|
|
640
708
|
mappings.append(node)
|
|
641
|
-
pending.extend(_type_children(node, semantics))
|
|
709
|
+
pending.extend(_type_children(node, semantics, shadowed=shadowed))
|
|
642
710
|
return mappings
|
|
643
711
|
|
|
644
712
|
|
|
645
|
-
def _is_any_mapping(node: ast.Subscript, semantics: AnnotationSemantics) -> bool:
|
|
713
|
+
def _is_any_mapping(node: ast.Subscript, semantics: AnnotationSemantics, *, shadowed: frozenset[str]) -> bool:
|
|
646
714
|
if not isinstance(node.slice, ast.Tuple) or len(node.slice.elts) != _MAPPING_ARGUMENT_COUNT:
|
|
647
715
|
return False
|
|
648
716
|
imports = semantics.imports
|
|
649
717
|
target = node.value
|
|
718
|
+
if _expression_root(target) in shadowed:
|
|
719
|
+
return False
|
|
650
720
|
mapping = (isinstance(target, ast.Name) and target.id == "dict" and imports.builtin_is_unshadowed("dict")) or any(
|
|
651
721
|
imports.resolves(target, sources=_MAPPING_SOURCES, symbol=name) for name in _MAPPING_NAMES
|
|
652
722
|
)
|
|
653
723
|
if not mapping:
|
|
654
724
|
return False
|
|
655
725
|
key, value = node.slice.elts
|
|
656
|
-
return
|
|
726
|
+
return (
|
|
727
|
+
_expression_root(key) not in shadowed
|
|
728
|
+
and _is_builtin_str(key, imports)
|
|
729
|
+
and _contains_any(value, semantics, seen=frozenset(), shadowed=shadowed)
|
|
730
|
+
)
|
|
657
731
|
|
|
658
732
|
|
|
659
733
|
def _is_builtin_str(node: ast.expr, imports: ImportIndex) -> bool:
|
|
@@ -665,20 +739,30 @@ def _contains_any(
|
|
|
665
739
|
semantics: AnnotationSemantics,
|
|
666
740
|
*,
|
|
667
741
|
seen: frozenset[str],
|
|
742
|
+
shadowed: frozenset[str] = frozenset(),
|
|
668
743
|
) -> bool:
|
|
669
744
|
parsed = semantics.parse(node)
|
|
670
|
-
if parsed is None:
|
|
745
|
+
if parsed is None or _expression_root(parsed) in shadowed:
|
|
671
746
|
return False
|
|
672
747
|
imports = semantics.imports
|
|
673
748
|
if imports.resolves(parsed, sources=_TYPING_SOURCES, symbol="Any"):
|
|
674
749
|
return True
|
|
675
750
|
if isinstance(parsed, ast.Name) and parsed.id in semantics.aliases and parsed.id not in seen:
|
|
751
|
+
# Module aliases retain their declaration scope, independent of a
|
|
752
|
+
# generic name at this annotation's use site.
|
|
676
753
|
return _contains_any(semantics.aliases[parsed.id], semantics, seen=seen | {parsed.id})
|
|
677
|
-
return any(
|
|
754
|
+
return any(
|
|
755
|
+
_contains_any(child, semantics, seen=seen, shadowed=shadowed)
|
|
756
|
+
for child in _type_children(parsed, semantics, shadowed=shadowed)
|
|
757
|
+
)
|
|
678
758
|
|
|
679
759
|
|
|
680
|
-
def _type_children(
|
|
760
|
+
def _type_children(
|
|
761
|
+
node: ast.expr, semantics: AnnotationSemantics, *, shadowed: frozenset[str] = frozenset()
|
|
762
|
+
) -> tuple[ast.expr, ...]:
|
|
681
763
|
if isinstance(node, ast.Subscript):
|
|
764
|
+
if _expression_root(node.value) in shadowed:
|
|
765
|
+
return ()
|
|
682
766
|
imports = semantics.imports
|
|
683
767
|
symbol = imports.resolved_symbol(node.value, sources=_TYPING_SOURCES | _MAPPING_SOURCES)
|
|
684
768
|
if symbol == "Literal":
|
|
@@ -132,7 +132,7 @@ class NoMutatingContainerTransform(Rule):
|
|
|
132
132
|
id = "no-mutating-container-transform"
|
|
133
133
|
code = "SARJ520"
|
|
134
134
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
135
|
-
default_level=Severity.
|
|
135
|
+
default_level=Severity.ERROR,
|
|
136
136
|
summary="Return container replacements without mutating borrowed builtin inputs.",
|
|
137
137
|
rationale="A container-valued transformation can change caller aliases before returning its result. Copying after the write leaves the original input changed.",
|
|
138
138
|
remediation="Copy each changed container before writing, return a fresh replacement, or expose a separate command returning None. Keep documented in-place contracts explicit; do not discard the operation or add a dummy assignment.",
|
|
@@ -203,7 +203,7 @@ class NoMutatingContainerTransform(Rule):
|
|
|
203
203
|
col=mutation.node.col_offset + 1,
|
|
204
204
|
code=self.code,
|
|
205
205
|
message=f"This container-valued transformation mutates borrowed {mutation.owner.kind} input `{mutation.owner.parameter}`; copy before writing or use a command returning None.",
|
|
206
|
-
severity=Severity.
|
|
206
|
+
severity=Severity.ERROR,
|
|
207
207
|
)
|
|
208
208
|
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef)
|
|
209
209
|
for mutation in analysis.mutations(function)
|
|
@@ -148,6 +148,7 @@ class NoRawSourceTextTestOracle(Rule):
|
|
|
148
148
|
autofix=AutofixPolicy.NONE,
|
|
149
149
|
aliases=("source-coupled-test",),
|
|
150
150
|
limitations=(
|
|
151
|
+
"A literal module-level __test__ = False disables pytest collection. Later bindings in eager module expressions or explicitly global class scopes clear that proof; class-local names, deferred bodies, and annotation-only names do not rebind it.",
|
|
151
152
|
"The rule recognizes source-like suffixes, shell templates ending in .sh.tftpl, and the exact Dockerfile basename; it follows local aliases, context-managed reads, and common normalization.",
|
|
152
153
|
"Direct generator/list-comprehension reads unpacked from stable module-level path tuples are followed; mutable collections, rebinding, filtered comprehensions, and interprocedural flows remain unreported.",
|
|
153
154
|
"Literal shebang startswith checks are representation contracts and remain unreported; other text compatibility sentinels require an exact suppression.",
|
|
@@ -186,6 +187,38 @@ class NoRawSourceTextTestOracle(Rule):
|
|
|
186
187
|
expected_count=1,
|
|
187
188
|
public=True,
|
|
188
189
|
),
|
|
190
|
+
RuleExample(
|
|
191
|
+
example_id="disabled-module-source-oracle",
|
|
192
|
+
scenario="module-collection",
|
|
193
|
+
title="An explicitly disabled module has no collected source oracle",
|
|
194
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
195
|
+
files=(
|
|
196
|
+
ExampleFile.python(
|
|
197
|
+
"tests/test_policy.py",
|
|
198
|
+
"__test__ = False\nfrom pathlib import Path\ndef test_policy():\n"
|
|
199
|
+
" source = Path('workflow.yml').read_text()\n assert 'permissions:' in source\n",
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
focus_path=PurePosixPath("tests/test_policy.py"),
|
|
203
|
+
expected_count=0,
|
|
204
|
+
public=True,
|
|
205
|
+
),
|
|
206
|
+
RuleExample(
|
|
207
|
+
example_id="enabled-module-source-oracle",
|
|
208
|
+
scenario="module-collection",
|
|
209
|
+
title="An enabled module still needs a parsed source contract",
|
|
210
|
+
outcome=ExampleOutcome.MATCH,
|
|
211
|
+
files=(
|
|
212
|
+
ExampleFile.python(
|
|
213
|
+
"tests/test_policy.py",
|
|
214
|
+
"__test__ = True\nfrom pathlib import Path\ndef test_policy():\n"
|
|
215
|
+
" source = Path('workflow.yml').read_text()\n assert 'permissions:' in source\n",
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
focus_path=PurePosixPath("tests/test_policy.py"),
|
|
219
|
+
expected_count=1,
|
|
220
|
+
public=True,
|
|
221
|
+
),
|
|
189
222
|
),
|
|
190
223
|
)
|
|
191
224
|
description = documentation.summary
|
|
@@ -233,6 +266,8 @@ class NoRawSourceTextTestOracle(Rule):
|
|
|
233
266
|
|
|
234
267
|
|
|
235
268
|
def top_level_test_functions(tree: ast.Module, imports: ImportIndex | None = None) -> list[TestFunction]:
|
|
269
|
+
if _module_collection_disabled(tree):
|
|
270
|
+
return []
|
|
236
271
|
resolved_imports = imports or ImportIndex.from_tree(tree, module_scope_only=True)
|
|
237
272
|
opted_out = _module_test_opt_outs(tree)
|
|
238
273
|
functions: list[TestFunction] = []
|
|
@@ -249,6 +284,116 @@ def top_level_test_functions(tree: ast.Module, imports: ImportIndex | None = Non
|
|
|
249
284
|
return functions
|
|
250
285
|
|
|
251
286
|
|
|
287
|
+
def _module_collection_disabled(tree: ast.Module) -> bool:
|
|
288
|
+
disabled = False
|
|
289
|
+
for statement in tree.body:
|
|
290
|
+
match statement:
|
|
291
|
+
case ast.Assign(targets=targets) if any(
|
|
292
|
+
isinstance(target, ast.Name) and target.id == "__test__" for target in targets
|
|
293
|
+
):
|
|
294
|
+
disabled = _assignment_disables_module_tests(statement)
|
|
295
|
+
case ast.AnnAssign(target=ast.Name(id="__test__"), value=value):
|
|
296
|
+
if value is not None:
|
|
297
|
+
disabled = isinstance(value, ast.Constant) and value.value is False
|
|
298
|
+
case _:
|
|
299
|
+
if disabled and _eagerly_binds_module_test(statement):
|
|
300
|
+
disabled = False
|
|
301
|
+
return disabled
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _assignment_disables_module_tests(statement: ast.Assign) -> bool:
|
|
305
|
+
value = statement.value
|
|
306
|
+
if not isinstance(value, ast.Constant) or value.value is not False:
|
|
307
|
+
return False
|
|
308
|
+
return not any(
|
|
309
|
+
_eagerly_binds_module_test(target) for target in statement.targets if not isinstance(target, ast.Name)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _eagerly_binds_module_test(node: ast.AST) -> bool:
|
|
314
|
+
pending = [(node, True)]
|
|
315
|
+
while pending:
|
|
316
|
+
current, module_scope = pending.pop()
|
|
317
|
+
if module_scope and _eager_node_binds_test(current):
|
|
318
|
+
return True
|
|
319
|
+
if isinstance(current, ast.ClassDef):
|
|
320
|
+
class_module_scope = _class_declares_global_test(current)
|
|
321
|
+
pending.extend((child, class_module_scope) for child in current.body)
|
|
322
|
+
pending.extend((child, module_scope) for child in _eager_scope_children(current))
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _eager_node_binds_test(node: ast.AST) -> bool:
|
|
327
|
+
match node:
|
|
328
|
+
case (
|
|
329
|
+
ast.FunctionDef(name="__test__")
|
|
330
|
+
| ast.AsyncFunctionDef(name="__test__")
|
|
331
|
+
| ast.ClassDef(name="__test__")
|
|
332
|
+
| ast.TypeAlias(name=ast.Name(id="__test__"))
|
|
333
|
+
| ast.ExceptHandler(name="__test__")
|
|
334
|
+
| ast.MatchAs(name="__test__")
|
|
335
|
+
| ast.MatchStar(name="__test__")
|
|
336
|
+
| ast.MatchMapping(rest="__test__")
|
|
337
|
+
| ast.Name(id="__test__", ctx=(ast.Store() | ast.Del()))
|
|
338
|
+
):
|
|
339
|
+
return True
|
|
340
|
+
case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
|
|
341
|
+
return any((alias.asname or alias.name.partition(".")[0]) in {"__test__", "*"} for alias in aliases)
|
|
342
|
+
case _:
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _eager_scope_children(node: ast.AST) -> list[ast.AST]:
|
|
347
|
+
match node:
|
|
348
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
349
|
+
# Headers run now; annotations and the body are deferred.
|
|
350
|
+
return [*node.decorator_list, *_eager_argument_defaults(node.args)]
|
|
351
|
+
case ast.ClassDef():
|
|
352
|
+
return [*node.decorator_list, *node.bases, *(keyword.value for keyword in node.keywords)]
|
|
353
|
+
case ast.Lambda():
|
|
354
|
+
return _eager_argument_defaults(node.args)
|
|
355
|
+
case ast.ListComp() | ast.SetComp() | ast.DictComp():
|
|
356
|
+
return _comprehension_eager_children(node)
|
|
357
|
+
case ast.GeneratorExp():
|
|
358
|
+
return [node.generators[0].iter]
|
|
359
|
+
case ast.TypeAlias() | ast.Import() | ast.ImportFrom():
|
|
360
|
+
return []
|
|
361
|
+
case ast.AnnAssign():
|
|
362
|
+
return _annotated_assignment_eager_children(node)
|
|
363
|
+
case _:
|
|
364
|
+
return list(ast.iter_child_nodes(node))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _eager_argument_defaults(arguments: ast.arguments) -> list[ast.AST]:
|
|
368
|
+
return [*arguments.defaults, *filter(None, arguments.kw_defaults)]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _comprehension_eager_children(node: ast.ListComp | ast.SetComp | ast.DictComp) -> list[ast.AST]:
|
|
372
|
+
# Iteration targets stay local; walrus bindings use the enclosing scope.
|
|
373
|
+
children: list[ast.AST] = [node.key, node.value] if isinstance(node, ast.DictComp) else [node.elt]
|
|
374
|
+
children.extend(generator.iter for generator in node.generators)
|
|
375
|
+
children.extend(condition for generator in node.generators for condition in generator.ifs)
|
|
376
|
+
return children
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _annotated_assignment_eager_children(node: ast.AnnAssign) -> list[ast.AST]:
|
|
380
|
+
if node.value is None:
|
|
381
|
+
return [] if isinstance(node.target, ast.Name) else [node.target]
|
|
382
|
+
return [node.target, node.value]
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _class_declares_global_test(node: ast.ClassDef) -> bool:
|
|
386
|
+
pending: list[ast.AST] = list(node.body)
|
|
387
|
+
while pending:
|
|
388
|
+
current = pending.pop()
|
|
389
|
+
if isinstance(current, ast.Global) and "__test__" in current.names:
|
|
390
|
+
return True
|
|
391
|
+
if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
|
|
392
|
+
continue
|
|
393
|
+
pending.extend(ast.iter_child_nodes(current))
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
|
|
252
397
|
def _class_test_functions(
|
|
253
398
|
statement: ast.ClassDef, resolved_imports: ImportIndex, opted_out: frozenset[str]
|
|
254
399
|
) -> list[TestFunction]:
|
|
@@ -20,7 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
20
20
|
Severity,
|
|
21
21
|
is_suppressed,
|
|
22
22
|
)
|
|
23
|
-
from sarj_python_lint.rules._ast_index import nodes, walk as walk_ast
|
|
23
|
+
from sarj_python_lint.rules._ast_index import nodes, parent_map, walk as walk_ast
|
|
24
24
|
from sarj_python_lint.rules._inherited_contract import inherited_contract
|
|
25
25
|
from sarj_python_lint.rules._nominal_project import NominalSource
|
|
26
26
|
|
|
@@ -155,7 +155,7 @@ class PreferNominalIdTypes(Rule):
|
|
|
155
155
|
"The rule checks functions, methods, constructors, and classes for at least two ID-shaped roles with the same proven carrier. Existing coverage remains an error.",
|
|
156
156
|
"Imported first-party carriers and non-ID roles backed by a unique matching NewType in the owning distribution or a declared local dependency are warnings. Ambiguous exports, conditional or relative imports, cycles, and unresolved ownership are skipped.",
|
|
157
157
|
"Non-ID role discovery uses exact snake-case names and proven matching carriers; it does not infer brands from names alone. Dependency discovery respects declared workspace members and exclusions, and excludes optional and transitive dependencies.",
|
|
158
|
-
"Rebound aliases, generic aliases, carrier names shadowed by type parameters, and mutated
|
|
158
|
+
"Rebound aliases, generic aliases, carrier names shadowed by type parameters, and mutated or escaped canonical provider namespaces are skipped conservatively across the file.",
|
|
159
159
|
"Generated code, migrations, external adapters, operational context, raw schemas, SQLAlchemy Mapped fields, ambiguous imports, and unlike carrier shapes are excluded.",
|
|
160
160
|
"Classes with explicit bases, including object, are excluded without requiring an override decorator. Local base declarations do not prove runtime member ownership remains closed; class construction, native hooks and escaped handles can change inheritance. Independent module functions and plain classes remain eligible.",
|
|
161
161
|
),
|
|
@@ -221,6 +221,21 @@ class PreferNominalIdTypes(Rule):
|
|
|
221
221
|
expected_count=0,
|
|
222
222
|
public=False,
|
|
223
223
|
),
|
|
224
|
+
RuleExample(
|
|
225
|
+
example_id="escaped-canonical-provider",
|
|
226
|
+
scenario="provider-identity",
|
|
227
|
+
title="An escaped factory namespace does not establish canonical domain roles",
|
|
228
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
229
|
+
files=(
|
|
230
|
+
ExampleFile.python(
|
|
231
|
+
"app/service.py",
|
|
232
|
+
"import typing\ndef replace(namespace):\n namespace.NewType = lambda name, value: value\nreplace(typing)\nRoomName = typing.NewType('RoomName', str)\nCallId = typing.NewType('CallId', str)\ndef record(room_name: str, call_id: str) -> None: ...\n",
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
236
|
+
expected_count=0,
|
|
237
|
+
public=False,
|
|
238
|
+
),
|
|
224
239
|
),
|
|
225
240
|
)
|
|
226
241
|
description: str = documentation.summary
|
|
@@ -611,22 +626,103 @@ def _carrier_shadowed_names(
|
|
|
611
626
|
) -> frozenset[str]:
|
|
612
627
|
# Match ImportIndex's conservative whole-file treatment of ambiguous bindings.
|
|
613
628
|
# PEP 695 parameters and mutations of imported namespaces also defeat identity.
|
|
614
|
-
shadowed
|
|
629
|
+
shadowed = _escaped_provider_names(tree, imports, node_index=node_index)
|
|
615
630
|
for node in nodes(tree, ast.TypeVar, ast.ParamSpec, ast.TypeVarTuple, ast.Attribute, ast.Call, index=node_index):
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
shadowed.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
or imports.resolves(node.func, sources=_BUILTIN_SOURCES, symbol=symbol)
|
|
624
|
-
for symbol in ("setattr", "delattr", "vars")
|
|
625
|
-
):
|
|
626
|
-
shadowed.add(ast.unparse(node.args[0]).partition(".")[0])
|
|
631
|
+
match node:
|
|
632
|
+
case ast.TypeVar() | ast.ParamSpec() | ast.TypeVarTuple():
|
|
633
|
+
shadowed.add(node.name)
|
|
634
|
+
case ast.Attribute():
|
|
635
|
+
shadowed.update(_attribute_provider_shadowed_names(node, imports))
|
|
636
|
+
case ast.Call():
|
|
637
|
+
shadowed.update(_call_provider_shadowed_names(node, imports))
|
|
627
638
|
return frozenset(shadowed)
|
|
628
639
|
|
|
629
640
|
|
|
641
|
+
def _attribute_provider_shadowed_names(node: ast.Attribute, imports: ImportIndex) -> set[str]:
|
|
642
|
+
if not isinstance(node.ctx, (ast.Store, ast.Del)) and node.attr not in {
|
|
643
|
+
"__dict__",
|
|
644
|
+
"__setattr__",
|
|
645
|
+
"__delattr__",
|
|
646
|
+
"__getattribute__",
|
|
647
|
+
}:
|
|
648
|
+
return set()
|
|
649
|
+
shadowed = {ast.unparse(node).partition(".")[0]}
|
|
650
|
+
if isinstance(node.value, ast.Name):
|
|
651
|
+
shadowed.update(_mutated_builtin_names(node.value, node.attr, imports))
|
|
652
|
+
return shadowed
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _call_provider_shadowed_names(node: ast.Call, imports: ImportIndex) -> set[str]:
|
|
656
|
+
if not node.args or not _is_builtin_namespace_call(node, imports):
|
|
657
|
+
return set()
|
|
658
|
+
shadowed = {ast.unparse(node.args[0]).partition(".")[0]}
|
|
659
|
+
member = (
|
|
660
|
+
node.args[1].value
|
|
661
|
+
if len(node.args) > 1 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str)
|
|
662
|
+
else None
|
|
663
|
+
)
|
|
664
|
+
shadowed.update(_mutated_builtin_names(node.args[0], member, imports))
|
|
665
|
+
return shadowed
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _is_builtin_namespace_call(node: ast.Call, imports: ImportIndex) -> bool:
|
|
669
|
+
return any(
|
|
670
|
+
(isinstance(node.func, ast.Name) and node.func.id == symbol and imports.builtin_is_unshadowed(symbol))
|
|
671
|
+
or imports.resolves(node.func, sources=_BUILTIN_SOURCES, symbol=symbol)
|
|
672
|
+
for symbol in ("setattr", "delattr", "vars")
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _escaped_provider_names(tree: ast.Module, imports: ImportIndex, *, node_index: NodeIndex | None) -> set[str]:
|
|
677
|
+
providers = _TYPING_SOURCES | _BUILTIN_SOURCES | _UUID_SOURCES
|
|
678
|
+
namespaces = {
|
|
679
|
+
name: binding.module
|
|
680
|
+
for name, binding in imports.bindings.items()
|
|
681
|
+
if binding.module in providers and binding.symbol is None
|
|
682
|
+
}
|
|
683
|
+
if not namespaces:
|
|
684
|
+
return set()
|
|
685
|
+
parents = parent_map(tree, index=node_index)
|
|
686
|
+
shadowed: set[str] = set()
|
|
687
|
+
for node in nodes(tree, ast.Name, index=node_index):
|
|
688
|
+
if not isinstance(node.ctx, ast.Load) or node.id not in namespaces:
|
|
689
|
+
continue
|
|
690
|
+
if _direct_provider_access(node, parents.get(node), imports):
|
|
691
|
+
continue
|
|
692
|
+
provider = namespaces[node.id]
|
|
693
|
+
shadowed.update(name for name, binding in imports.bindings.items() if binding.module == provider)
|
|
694
|
+
shadowed.update(_mutated_builtin_names(node, None, imports))
|
|
695
|
+
return shadowed
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _direct_provider_access(node: ast.Name, parent: ast.AST | None, imports: ImportIndex) -> bool:
|
|
699
|
+
if isinstance(parent, ast.Attribute) and parent.value is node:
|
|
700
|
+
return True
|
|
701
|
+
return (
|
|
702
|
+
isinstance(parent, ast.Call)
|
|
703
|
+
and bool(parent.args)
|
|
704
|
+
and parent.args[0] is node
|
|
705
|
+
and _is_builtin_namespace_call(parent, imports)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _mutated_builtin_names(node: ast.expr, member: str | None, imports: ImportIndex) -> set[str]:
|
|
710
|
+
if not isinstance(node, ast.Name):
|
|
711
|
+
return set()
|
|
712
|
+
binding = imports.bindings.get(node.id)
|
|
713
|
+
if binding is None or binding.module != "builtins" or binding.symbol is not None:
|
|
714
|
+
return set()
|
|
715
|
+
carriers = {"str", "int"} | _BUILTIN_COLLECTIONS
|
|
716
|
+
changed = carriers if member is None or member.startswith("__") else carriers & {member}
|
|
717
|
+
if not changed:
|
|
718
|
+
return set()
|
|
719
|
+
return changed | {
|
|
720
|
+
name
|
|
721
|
+
for name, imported in imports.bindings.items()
|
|
722
|
+
if imported.module == "builtins" and (imported.symbol is None or imported.symbol in changed)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
|
|
630
726
|
def _expression_is_shadowed(expression: ast.expr, names: frozenset[str]) -> bool:
|
|
631
727
|
return bool(names) and any(isinstance(node, ast.Name) and node.id in names for node in walk_ast(expression))
|
|
632
728
|
|